Overview

This notebook evaluates the predicted rates of forgetting. The predictions are compared to the value derived at the end of each learning session to determine their accuracy.

Setup

library(fst)
library(data.table)
library(tidyr)
library(purrr)

Attaching package: 'purrr'
The following object is masked from 'package:data.table':

    transpose
library(furrr)
Loading required package: future
library(stringr)
library(ggplot2)
library(wesanderson)
library(lme4)
Loading required package: Matrix

Attaching package: 'Matrix'
The following objects are masked from 'package:tidyr':

    expand, pack, unpack
library(lmerTest)

Attaching package: 'lmerTest'
The following object is masked from 'package:lme4':

    lmer
The following object is masked from 'package:stats':

    step
library(multcomp)
Loading required package: mvtnorm
Loading required package: survival

Attaching package: 'survival'
The following object is masked from 'package:future':

    cluster
Loading required package: TH.data
Loading required package: MASS

Attaching package: 'TH.data'
The following object is masked from 'package:MASS':

    geyser
library(emmeans)
source(file.path("..", "scripts", "99_slimstampen_model_funs.R"))

Attaching package: 'dplyr'
The following object is masked from 'package:MASS':

    select
The following objects are masked from 'package:data.table':

    between, first, last
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
future::plan("multisession", workers = 6) # Set to desired number of cores
theme_set(theme_light(base_size = 14) +
            theme(strip.text = element_text(colour = "black")))

condition_colours <- wes_palette("Darjeeling1", n = 5)
condition_colours[c(2, 4, 5)] <- condition_colours[c(4, 5, 2)]

dataset_colours <- wes_palette("Darjeeling2", n = 5)[c(2, 3)]

Helper functions

load_predictions <- function(course) {
  
  pred_user <- read_fst(file.path("..", "data", "predictions", paste0("pred_v_obs_user_", str_replace_all(course, " ", "_"), ".fst")))
  pred_fact <- read_fst(file.path("..", "data", "predictions", paste0("pred_v_obs_fact_", str_replace_all(course, " ", "_"), ".fst")))
  pred_fact_user <- read_fst(file.path("..", "data", "predictions", paste0("pred_fact_and_user_", str_replace_all(course, " ", "_"), ".fst")))
  setDT(pred_user)
  setDT(pred_fact)
  setDT(pred_fact_user)
  
  pred_domain <- mean(unique(pred_fact, by = c("fact_id"))$pred_fact)
  pred_default <- 0.3
  
  # Combine
  pred_all <- merge(pred_user, pred_fact, by = c("user_id", "fact_id", "alpha", "n_reps"), all = TRUE)
  pred_all <- merge(pred_all, pred_fact_user, by = c("user_id", "fact_id", "alpha"), all = TRUE)
  pred_all[, pred_default := pred_default]
  pred_all[, pred_domain := pred_domain]
  
  pred_obs_long <- pivot_longer(pred_all, 
                                cols = pred_user:pred_domain,
                                names_to = "prediction_type",
                                names_prefix = "pred\\_")
  
  setDT(pred_obs_long)
  
  # Remove NA predictions and predictions without corresponding observations
  pred_obs_long <- pred_obs_long[!is.na(value)]
  pred_obs_long <- pred_obs_long[!is.na(alpha)]
  
  # Remove duplicates
  pred_obs_long <- unique(pred_obs_long)
  
  # Set proper labels
  condition_labels <- data.table(prediction_type = c("default", "domain", "fact", "user", "fact_user"),
                                 prediction_label = factor(c("Default", "Domain", "Fact", "Learner", "Fact & Learner"),
                                                           levels = c("Default", "Domain", "Fact", "Learner", "Fact & Learner")))
  pred_obs_long <- pred_obs_long[condition_labels, on = .(prediction_type)]
  
  return(pred_obs_long)
}

Rate of forgetting

Predicted rate of forgetting

pred_gl <- load_predictions("Grandes Lignes")
pred_ss <- load_predictions("Stepping Stones")

pred_both <- rbind(pred_gl[, course := "French"],
                   pred_ss[, course := "English"])

Distribution of predictions

p_rof_dist <- ggplot(pred_both, aes(x = value, fill = prediction_label)) +
  facet_grid(prediction_label ~ course, scales = "free_y") +
  geom_histogram(aes(y = ..density..), binwidth = .01) +
  guides(fill = "none") +
  labs(x = "Predicted Speed of Forgetting",
       y = "Density") +
  scale_fill_manual(values = condition_colours)

p_rof_dist
Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
ℹ Please use `after_stat(density)` instead.
This warning is displayed once every 8 hours.
Call `lifecycle::last_lifecycle_warnings()` to see where this warning was generated.

ggsave(plot = p_rof_dist, file.path("..", "output", "rof_predictions_distribution.png"),
       device = "png", width = 5, height = 7.5)

Predicted vs. observed values

We compare the predicted rate of forgetting to the “observed” rate of forgetting, i.e., the rate of forgetting that was estimated at the end of the learning sequence.

To assess the accuracy of predictions, we compute the mean absolute error (MAE) as an aggregate statistic, as well as the absolute error (AE) of each individual prediction.

pred_mae <- pred_both[, .(mae = mean(abs(alpha - value)),
                          ae_se = sd(abs(alpha - value))/sqrt(.N)), 
                      by = .(course, prediction_label)]

  
n_obs <- pred_both[, .N, by = .(course, prediction_label)]

Plot predicted vs. observed values:

rof_min <- 0
rof_max <- 1
rof_breaks <- seq(0.1, 0.9, by = .2)

p_rof_pred_v_obs <- ggplot(pred_both,
                         aes(x = value, y = alpha, colour = prediction_label)) +
    facet_grid(course ~ prediction_label) +
    geom_hline(yintercept = 0.3, lty = 2) +
    geom_vline(xintercept = 0.3, lty = 2) +
    geom_abline(slope = 1, intercept = 0, lty = 3, alpha = 0.75) +
    geom_point(alpha = .1, size = .1, pch = ".") +
    geom_smooth(method = "lm", formula = y ~ x, colour = "black") +
  geom_label(data = pred_mae,
             aes(label = paste("MAE =", formatC(mae, digits = 3, flag = "#"))),
             x = rof_max, y = rof_min,
             hjust = 1, colour = "NA", size = 3,
             alpha = .9,
             label.size = NA) +
  geom_text(data = pred_mae,
            aes(label = paste("MAE =", formatC(mae, digits = 3, flag = "#"))),
            x = rof_max, y = rof_min,
            hjust = 1, colour = "black", size = 3) +
  geom_label(data = n_obs,
             aes(label = paste("n =", scales::comma(N))),
             x = rof_max,
             y = rof_max,
             hjust = 1, colour = "NA", size = 3,
             alpha = .9,
             label.size = NA) +
  geom_text(data = n_obs,
            aes(label = paste("n =", scales::comma(N))),
            x = rof_max,
            y = rof_max,
            hjust = 1, colour = "black", size = 3) +
  guides(colour = "none") +
  labs(x = "Predicted Speed of Forgetting α",
       y = "Observed Speed of Forgetting α") +
  coord_fixed(ratio = 1, xlim = c(rof_min, rof_max), ylim = c(rof_min, rof_max)) +
  scale_x_continuous(breaks = rof_breaks) +
  scale_y_continuous(breaks = rof_breaks) +
  scale_colour_manual(values = condition_colours)

p_rof_pred_v_obs

ggsave(plot = p_rof_pred_v_obs, file.path("..", "output", "rof_predicted_vs_observed.png"),
  device = "png", width = 10, height = 4.5)

rm(p_rof_pred_v_obs)

Prediction error

Calculate prediction error:

pred_both[, prediction_error := value - alpha]

Distribution of prediction error:

p_rof_pred_error <- ggplot(pred_both, aes(x = prediction_error, fill = prediction_label)) +
  facet_grid(prediction_label ~ course, scales = "free_y") +
  geom_histogram(aes(y = ..density..), binwidth = .01) +
  guides(fill = "none") +
  labs(x = "SoF prediction error (predicted - observed)",
       y = "Density") +
  scale_fill_manual(values = condition_colours)

p_rof_pred_error

ggsave(plot = p_rof_pred_error, file.path("..", "output", "rof_prediction_error.png"),
       device = "png", width = 5, height = 7.5)

Absolute prediction error

To compare the magnitude of prediction errors between prediction methods, we look at absolute prediction error.

pred_both[, abs_prediction_error := abs(prediction_error)]

Distribution of absolute prediction error:

p_rof_abs_pred_error <- ggplot(pred_both, aes(x = abs_prediction_error, fill = prediction_label)) +
  facet_grid(prediction_label ~ course, scales = "free_y") +
  geom_histogram(aes(y = ..density..), binwidth = .01) +
  guides(fill = "none") +
  labs(x = "Absolute SoF prediction error",
       y = "Density") +
  scale_fill_manual(values = condition_colours)

p_rof_abs_pred_error

ggsave(plot = p_rof_abs_pred_error, file.path("..", "output", "rof_absolute_prediction_error.png"),
       device = "png", width = 5, height = 7.5)
pred_error_summarised <- pred_both[, .(error_mean = mean(abs_prediction_error), error_se = sd(abs_prediction_error)/sqrt(.N)), by = .(course, prediction_label)]

ggplot(pred_error_summarised, aes(x = prediction_label, y = error_mean, colour = course)) +
  geom_boxplot(data = pred_both,
               aes(y = abs_prediction_error, group = interaction(course, prediction_label)),
               colour = "grey70",
               width = .25,
               outlier.shape = NA,
               position = position_dodge(width = .5)) +
  geom_errorbar(aes(ymin = error_mean - error_se, ymax = error_mean + error_se), width = 0, position = position_dodge(width = .5)) +
  geom_point(position = position_dodge(width = .5)) +
  coord_cartesian(ylim = c(0, 0.175)) +
  labs(x = "Method",
       y = "Absolute SoF prediction error",
       colour = "Course")

Fit a regression model on absolute prediction error. The whole data set is too big to fit in a reasonable time, so we fit the model to a random subset of 1M predictions (which already takes ~24hrs).

Grandes Lignes
m_rof_pred_error_gl_file <- file.path("..", "data", "model_fits", "m_pred_error_Grandes_Lignes_1e6.rda")

if (file.exists(m_rof_pred_error_gl_file)) {
  load(m_rof_pred_error_gl_file)
} else {
  
  pred_gl_reg <- (
    pred_both
    [course == "French"]
    [sample(.N, 1e6), .(prediction_label, abs_prediction_error, user_id, fact_id)]
  )
  
  m_rof_pred_error_gl <- lmer(abs_prediction_error ~ prediction_label + 
                                (1 | user_id) + (1 | fact_id),
                              data = pred_gl_reg)
  
  save(m_rof_pred_error_gl, file = m_rof_pred_error_gl_file)
}

summary(m_rof_pred_error_gl)
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: abs_prediction_error ~ prediction_label + (1 | user_id) + (1 |  
    fact_id)
   Data: pred_gl_reg

REML criterion at convergence: -3273096

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.6381 -0.5902 -0.1803  0.3945 14.5510 

Random effects:
 Groups   Name        Variance  Std.Dev.
 user_id  (Intercept) 0.0001461 0.01209 
 fact_id  (Intercept) 0.0001916 0.01384 
 Residual             0.0020919 0.04574 
Number of obs: 1000000, groups:  user_id, 40965; fact_id, 22884

Fixed effects:
                                 Estimate Std. Error         df t value
(Intercept)                     6.252e-02  1.701e-04  5.391e+04 367.666
prediction_labelDomain         -1.430e-03  1.446e-04  9.734e+05  -9.892
prediction_labelFact           -1.256e-02  1.454e-04  9.736e+05 -86.340
prediction_labelLearner        -4.216e-03  1.465e-04  9.744e+05 -28.786
prediction_labelFact & Learner -1.219e-02  1.474e-04  9.746e+05 -82.745
                               Pr(>|t|)    
(Intercept)                      <2e-16 ***
prediction_labelDomain           <2e-16 ***
prediction_labelFact             <2e-16 ***
prediction_labelLearner          <2e-16 ***
prediction_labelFact & Learner   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) prdc_D prdc_F prdc_L
prdctn_lblD -0.425                     
prdctn_lblF -0.416  0.496              
prdctn_lblL -0.413  0.493  0.490       
prdctn_lF&L -0.404  0.490  0.488  0.486

Compare different prediction types to each other:

ht_gl <- glht(m_rof_pred_error_gl, linfct = mcp(prediction_label = "Tukey"))
summary(ht_gl)

     Simultaneous Tests for General Linear Hypotheses

Multiple Comparisons of Means: Tukey Contrasts


Fit: lmer(formula = abs_prediction_error ~ prediction_label + (1 | 
    user_id) + (1 | fact_id), data = pred_gl_reg)

Linear Hypotheses:
                                Estimate Std. Error z value Pr(>|z|)    
Domain - Default == 0         -0.0014305  0.0001446  -9.892   <0.001 ***
Fact - Default == 0           -0.0125572  0.0001454 -86.340   <0.001 ***
Learner - Default == 0        -0.0042160  0.0001465 -28.786   <0.001 ***
Fact & Learner - Default == 0 -0.0121938  0.0001474 -82.745   <0.001 ***
Fact - Domain == 0            -0.0111266  0.0001456 -76.442   <0.001 ***
Learner - Domain == 0         -0.0027855  0.0001466 -18.999   <0.001 ***
Fact & Learner - Domain == 0  -0.0107633  0.0001475 -72.977   <0.001 ***
Learner - Fact == 0            0.0083412  0.0001474  56.590   <0.001 ***
Fact & Learner - Fact == 0     0.0003633  0.0001482   2.452    0.102    
Fact & Learner - Learner == 0 -0.0079779  0.0001490 -53.534   <0.001 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Adjusted p values reported -- single-step method)

Use emmeans to estimate the standardised effect size for each contrast:

emm_gl <- emmeans(m_rof_pred_error_gl, "prediction_label", lmer.df = "asymptotic")
# Sigma calculation follows the example in the eff_size documentation (see also Westfall et al., 2014)
vc_gl <- as.data.frame(VarCorr(m_rof_pred_error_gl)) 
sigma_gl <- sqrt(sum(vc_gl$vcov))
eff_size_gl <- eff_size(emm_gl, sigma = sigma_gl, edf = Inf) # Choice of edf does not affect effect size, only the SE
eff_size_gl
 contrast                 effect.size      SE  df asymp.LCL asymp.UCL
 Default - Domain             0.02902 0.00293 Inf    0.0233   0.03477
 Default - Fact               0.25475 0.00295 Inf    0.2490   0.26054
 Default - Learner            0.08553 0.00297 Inf    0.0797   0.09135
 Default - Fact & Learner     0.24738 0.00299 Inf    0.2415   0.25324
 Domain - Fact                0.22573 0.00295 Inf    0.2199   0.23152
 Domain - Learner             0.05651 0.00297 Inf    0.0507   0.06234
 Domain - Fact & Learner      0.21836 0.00299 Inf    0.2125   0.22422
 Fact - Learner              -0.16922 0.00299 Inf   -0.1751  -0.16336
 Fact - Fact & Learner       -0.00737 0.00301 Inf   -0.0133  -0.00148
 Learner - Fact & Learner     0.16185 0.00302 Inf    0.1559   0.16778

sigma used for effect sizes: 0.04929 
Degrees-of-freedom method: inherited from asymptotic when re-gridding 
Confidence level used: 0.95 

Inspect the model’s residuals:

qqnorm(resid(m_rof_pred_error_gl))
qqline(resid(m_rof_pred_error_gl), col = "red")

plot(m_rof_pred_error_gl)

The QQ plot indicates quite a strong skew, which is not surprising, given that the distribution of absolute error is bounded by zero on the left but unbounded on the right. Assuming a Gamma distribution may be better, but models that use a Gamma distribution do not converge here. The LMER also gives a sufficiently accurate estimate of the means.

Stepping Stones
m_rof_pred_error_ss_file <- file.path("..", "data", "model_fits", "m_pred_error_Stepping_Stones_1e6.rda")

if (file.exists(m_rof_pred_error_ss_file)) {
  load(m_rof_pred_error_ss_file)
} else {
  
  pred_ss_reg <- (
    pred_both
    [course == "English"]
    [sample(.N, 1e6), .(prediction_label, abs_prediction_error, user_id, fact_id)]
  )
  
  m_pred_error <- lmer(abs_prediction_error ~ prediction_label + 
                         (1 | user_id) + (1 | fact_id),
                       data = pred_ss_reg)
  
  save(m_pred_error, file = m_rof_pred_error_ss_file)
}

summary(m_pred_error)
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: abs_prediction_error ~ prediction_label + (1 | user_id) + (1 |  
    fact_id)
   Data: pred_ss_reg

REML criterion at convergence: -3582447

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.1157 -0.5163 -0.1535  0.2167 16.6397 

Random effects:
 Groups   Name        Variance  Std.Dev.
 user_id  (Intercept) 0.0001343 0.01159 
 fact_id  (Intercept) 0.0001022 0.01011 
 Residual             0.0014998 0.03873 
Number of obs: 1000000, groups:  user_id, 86084; fact_id, 45580

Fixed effects:
                                 Estimate Std. Error         df t value
(Intercept)                     5.297e-02  1.188e-04  1.484e+05  445.85
prediction_labelDomain         -2.163e-03  1.244e-04  9.714e+05  -17.39
prediction_labelFact           -7.743e-03  1.249e-04  9.710e+05  -62.02
prediction_labelLearner        -5.282e-03  1.255e-04  9.705e+05  -42.08
prediction_labelFact & Learner -7.926e-03  1.260e-04  9.701e+05  -62.91
                               Pr(>|t|)    
(Intercept)                      <2e-16 ***
prediction_labelDomain           <2e-16 ***
prediction_labelFact             <2e-16 ***
prediction_labelLearner          <2e-16 ***
prediction_labelFact & Learner   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) prdc_D prdc_F prdc_L
prdctn_lblD -0.522                     
prdctn_lblF -0.517  0.497              
prdctn_lblL -0.513  0.495  0.493       
prdctn_lF&L -0.509  0.493  0.492  0.489

Compare different prediction types to each other:

ht_ss <- glht(m_pred_error, linfct = mcp(prediction_label = "Tukey"))
summary(ht_ss)

     Simultaneous Tests for General Linear Hypotheses

Multiple Comparisons of Means: Tukey Contrasts


Fit: lmer(formula = abs_prediction_error ~ prediction_label + (1 | 
    user_id) + (1 | fact_id), data = pred_ss_reg)

Linear Hypotheses:
                                Estimate Std. Error z value Pr(>|z|)    
Domain - Default == 0         -0.0021631  0.0001244 -17.393   <1e-04 ***
Fact - Default == 0           -0.0077430  0.0001249 -62.018   <1e-04 ***
Learner - Default == 0        -0.0052823  0.0001255 -42.083   <1e-04 ***
Fact & Learner - Default == 0 -0.0079259  0.0001260 -62.907   <1e-04 ***
Fact - Domain == 0            -0.0055799  0.0001249 -44.665   <1e-04 ***
Learner - Domain == 0         -0.0031192  0.0001256 -24.834   <1e-04 ***
Fact & Learner - Domain == 0  -0.0057629  0.0001261 -45.717   <1e-04 ***
Learner - Fact == 0            0.0024607  0.0001261  19.518   <1e-04 ***
Fact & Learner - Fact == 0    -0.0001830  0.0001265  -1.447    0.597    
Fact & Learner - Learner == 0 -0.0026437  0.0001271 -20.798   <1e-04 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Adjusted p values reported -- single-step method)

Use emmeans to estimate the standardised effect size for each contrast:

emm_ss <- emmeans(m_pred_error, "prediction_label", lmer.df = "asymptotic")
# Sigma calculation follows the example in the eff_size documentation (see also Westfall et al., 2014)
vc_ss <- as.data.frame(VarCorr(m_pred_error)) 
sigma_ss <- sqrt(sum(vc_ss$vcov))
eff_size_ss <- eff_size(emm_ss, sigma = sigma_ss, edf = Inf) # Choice of edf does not affect effect size, only the SE
eff_size_ss
 contrast                 effect.size      SE  df asymp.LCL asymp.UCL
 Default - Domain             0.05191 0.00298 Inf   0.04606    0.0578
 Default - Fact               0.18582 0.00300 Inf   0.17994    0.1917
 Default - Learner            0.12676 0.00301 Inf   0.12086    0.1327
 Default - Fact & Learner     0.19021 0.00302 Inf   0.18428    0.1961
 Domain - Fact                0.13391 0.00300 Inf   0.12803    0.1398
 Domain - Learner             0.07485 0.00301 Inf   0.06895    0.0808
 Domain - Fact & Learner      0.13830 0.00303 Inf   0.13237    0.1442
 Fact - Learner              -0.05905 0.00303 Inf  -0.06498   -0.0531
 Fact - Fact & Learner        0.00439 0.00303 Inf  -0.00156    0.0103
 Learner - Fact & Learner     0.06344 0.00305 Inf   0.05746    0.0694

sigma used for effect sizes: 0.04167 
Degrees-of-freedom method: inherited from asymptotic when re-gridding 
Confidence level used: 0.95 

Inspect the model’s residuals:

qqnorm(resid(m_pred_error))
qqline(resid(m_pred_error), col = "red")

Comparison
ht_gl_tidy <- broom::tidy(confint(ht_gl))
ht_ss_tidy <- broom::tidy(confint(ht_ss))
setDT(ht_gl_tidy)
setDT(ht_ss_tidy)

ht_both_tidy <- rbind(ht_gl_tidy[, course := "French"],
                      ht_ss_tidy[, course := "English"])
p_rof_pred_error_comp <- ggplot(ht_both_tidy, aes(x = contrast, y = estimate, ymin = conf.low, ymax = conf.high, colour = course)) +
  geom_hline(yintercept = 0, linetype = "11", colour = "grey60") +
  geom_errorbar(width = 0.1) + 
  geom_point() +
  labs(x = "Linear hypotheses",
       y = "Estimate",
       caption = "Tukey's range test. Error bars show 95% family-wise confidence level.",
       colour = "Course") +
    coord_flip()

p_rof_pred_error_comp

ggsave(plot = p_rof_pred_error_comp, file.path("..", "output", "rof_prediction_error_comparisons.png"),
       device = "png", width = 7.5, height = 5)
Summary plot
# Set significance level of comparisons manually, based on model output
pred_error_summarised$comparison <- c("***")
pred_error_summarised[c(3, 8), comparison := NA]
pred_error_summarised[c(5, 10), comparison := "n.s."]
# Add fitted values
pred_error_summarised[course == "French", error_fitted := predict(m_rof_pred_error_gl,
                                                newdata = pred_error_summarised[course == "French"],
                                                re.form = NA, 
                                                type = "response")]
pred_error_summarised[course == "English", error_fitted := predict(m_pred_error,
                                                newdata = pred_error_summarised[course == "English"],
                                                re.form = NA, 
                                                type = "response")]
p_rof_abs_pred_error_summ <- ggplot(pred_error_summarised, aes(x = reorder(prediction_label, -error_mean), y = error_mean, colour = course)) +
  geom_errorbar(aes(ymin = error_mean - error_se, ymax = error_mean + error_se), width = 0) +
  geom_line(aes(group = course), lty = 2) +
  geom_point() +
  geom_text(aes(label = comparison), 
            colour = "black",
            position = position_nudge(x = .5, y = c(rep(0, 9), .001)),
            hjust = .5) +
  labs(x = "Method",
       y = "Absolute SoF prediction error",
       colour = "Course") +
  scale_colour_manual(values = dataset_colours) +
  theme(legend.position = c(.85, .85))

p_rof_abs_pred_error_summ
Warning: Removed 2 rows containing missing values (`geom_text()`).

ggsave(plot = p_rof_abs_pred_error_summ, file.path("..", "output", "rof_absolute_prediction_error_summary.png"),
       device = "png", width = 7.5, height = 4.5)
Warning: Removed 2 rows containing missing values (`geom_text()`).
pred_error_summarised[, prediction_rank := frank(-error_mean), by = .(course)]
annotation_df_ss <- data.table(
  course = rep("English", 10),
  start = c(1, 1, 1, 1,
            2, 2, 2,
            3, 3,
            4
  ),
  end = c(2, 3, 4, 5,
          3, 4, 5,
          4, 5,
          5
  ),
  y = seq(max(pred_error_summarised$error_mean)*1.01 + .00675, max(pred_error_summarised$error_mean)*1.01, by = -.00075),
  label = c("p < .001", "p < .001", "p < .001", "p < .001",
            "p < .001", "p < .001", "p < .001",
            "p < .001", "p < .001",
            "n.s.")
)

annotation_df_gl <- data.table(
  course = rep("French", 10),
  start = c(1, 1, 1, 1,
            2, 2, 2,
            3, 3,
            4
  ),
  end = c(2, 3, 4, 5,
          3, 4, 5,
          4, 5,
          5
  ),
  y = seq(max(pred_error_summarised$error_mean)*1.01 + .00675, max(pred_error_summarised$error_mean)*1.01, by = -.00075),
  label = c("p < .001", "p < .001", "p < .001", "p < .001",
            "p < .001", "p < .001", "p < .001",
            "p < .001", "p < .001",
            "n.s.")
)

annotation_df_rof <- rbind(annotation_df_ss, annotation_df_gl)
annotation_df_rof[, label := factor(label, levels = c("p < .001", "p < .01", "p < .05", "n.s."))]
p_rof_pred_error_summary <- ggplot(pred_error_summarised, aes(x = prediction_rank, y = error_mean)) +
  facet_grid(~ course) +
  geom_line(data = annotation_df_rof,
            aes(x = 1, y = .05, lty = label, alpha = label, colour = NULL)) + # Dummy line to get legend
  geom_line(aes(colour = course, group = course)) +
  geom_errorbar(aes(ymin = error_mean - error_se, ymax = error_mean + error_se), width = 0) +
  geom_point(aes(colour = course, group = course)) +
  geom_label(aes(label = prediction_label), 
             colour = "black", 
             alpha = .9,
             label.size = NA, 
             nudge_y = -.0025) +
  labs(x = NULL,
       y = "Absolute prediction error:\nSpeed of Forgetting α",
       colour = "Course") +
  scale_x_continuous(expand = expansion(add = .75), breaks = NULL) +
  scale_y_continuous(limits = c(0, NA)) +
  scale_colour_manual(values = dataset_colours) +
  scale_linetype_manual(values = c("p < .001" = 1,
                                   "p < .01" = 5,
                                   "p < .05" = 2,
                                   "n.s." = 3),
                        drop = FALSE,
                        name = "Pairwise comparison:") +
  scale_alpha_manual(values = c("p < .001" = 1,
                                "p < .01" = .75,
                                "p < .05" = .5, 
                                "n.s." = .25),
                     drop = FALSE,
                     name = "Pairwise comparison:") +
  guides(colour = "none") +
  ggsignif::geom_signif(data = annotation_df_rof,
                        aes(xmin = start, xmax = end, annotations = "", 
                            y_position = y, lty = label, alpha = label),
                        tip_length = 0,
                        manual = TRUE)  +
  theme(legend.position = "bottom",
        legend.justification = "right")
Warning in ggsignif::geom_signif(data = annotation_df_rof, aes(xmin = start, :
Ignoring unknown aesthetics: xmin, xmax, annotations, and y_position
p_rof_pred_error_summary
Warning: The following aesthetics were dropped during statistical transformation: xmin, xmax, y_position
ℹ This can happen when ggplot fails to infer the correct grouping structure in the data.
ℹ Did you forget to specify a `group` aesthetic or to convert a numerical variable into a factor?
Warning: The following aesthetics were dropped during statistical transformation: xmin, xmax, y_position
ℹ This can happen when ggplot fails to infer the correct grouping structure in the data.
ℹ Did you forget to specify a `group` aesthetic or to convert a numerical variable into a factor?

ggsave(file.path("..", "output", "rof_absolute_prediction_error_summary.png"),
       device = "png", width = 10, height = 8)
Warning: The following aesthetics were dropped during statistical transformation: xmin, xmax, y_position
ℹ This can happen when ggplot fails to infer the correct grouping structure in the data.
ℹ Did you forget to specify a `group` aesthetic or to convert a numerical variable into a factor?
The following aesthetics were dropped during statistical transformation: xmin, xmax, y_position
ℹ This can happen when ggplot fails to infer the correct grouping structure in the data.
ℹ Did you forget to specify a `group` aesthetic or to convert a numerical variable into a factor?

Improvement

How big was the improvement from worst to best prediction method?

French:

# Absolute change
ht_gl_tidy[contrast == "Fact - Default", estimate[1]]
[1] -0.01255716
# % change
scales::percent(
  ht_gl_tidy[contrast == "Fact - Default", estimate[1]] / fixef(m_rof_pred_error_gl)[[1]],
  accuracy = .1)
[1] "-20.1%"
-20.1%
# Associated standardised effect size
eff_size_gl_tidy <- broom::tidy(eff_size_gl) |> as.data.table()
eff_size_gl_tidy[contrast == "Default - Fact", estimate]
[1] 0.2547525

English:

# Absolute change
ht_ss_tidy[contrast == "Fact & Learner - Default", estimate[1]]
[1] -0.007925937
# % change
scales::percent(
  ht_ss_tidy[contrast == "Fact & Learner - Default", estimate[1]] / fixef(m_pred_error)[[1]],
  accuracy = .1)
[1] "-15.0%"
-15.0%
# Associated standardised effect size
eff_size_ss_tidy <- broom::tidy(eff_size_ss) |> as.data.table()
eff_size_ss_tidy[contrast == "Default - Fact & Learner", estimate]
[1] 0.1902057

Visualise prediction error

By learner

user_freq <- pred_both[, .N, by = .(course, prediction_label, user_id)]

pred_user_freq <- pred_both[user_freq[N > 50], on = .(course, prediction_label, user_id)]

pred_user_q <- pred_user_freq[, .(stat = c("whisker_low", "q25", "median", "q75", "whisker_high"),
                                  value = boxplot.stats(prediction_error, do.conf = FALSE, do.out = FALSE)$stats), by = .(course, prediction_label, user_id)]

pred_user_q <- pivot_wider(pred_user_q, names_from = "stat", values_from = "value")

pred_user_q <- pred_user_q %>%
  arrange(course, prediction_label, median) %>%
  group_by(course, prediction_label) %>%
  mutate(user_order = (1:n())/n())
ggplot(pred_user_q, aes(x = user_order)) +
  facet_grid(course ~ prediction_label) +
  geom_ribbon(aes(ymin = whisker_low, ymax = whisker_high, fill = course), alpha = .3) +
  geom_ribbon(aes(ymin = q25, ymax = q75, fill = course), alpha = .5) +
  geom_line(aes(y = median), lwd = 1) +
  geom_hline(data = NULL, yintercept = 0, lty = 3) +
    labs(x = "Learners",
       y = "SoF prediction error\n(predicted - observed)") +
  scale_fill_manual(values = dataset_colours) +
  guides(fill = "none") +
  theme(axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    panel.grid.major.x = element_blank()) +
  NULL

ggsave(file.path("..", "output", "rof_prediction_error_by_learner.png"),
  device = "png", width = 10, height = 4.5)

Absolute error:

pred_user_q <- pred_user_freq[, .(stat = c("whisker_low", "q25", "median", "q75", "whisker_high"),
                                  value = boxplot.stats(abs_prediction_error, do.conf = FALSE, do.out = FALSE)$stats), by = .(course, prediction_label, user_id)]

pred_user_q <- pivot_wider(pred_user_q, names_from = "stat", values_from = "value")

pred_user_q <- pred_user_q %>%
  arrange(course, prediction_label, median) %>%
  group_by(course, prediction_label) %>%
  mutate(user_order = (1:n())/n())

ggplot(pred_user_q, aes(x = user_order)) +
  facet_grid(course ~ prediction_label) +
  geom_ribbon(aes(ymin = whisker_low, ymax = whisker_high, fill = course), alpha = .3) +
  geom_ribbon(aes(ymin = q25, ymax = q75, fill = course), alpha = .5) +
  geom_line(aes(y = median), lwd = 1) +
    labs(x = "Learners",
       y = "Absolute SoF prediction error") +
  scale_fill_manual(values = dataset_colours) +
  guides(fill = "none") +
  theme(axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    panel.grid.major.x = element_blank()) +
  NULL

ggsave(file.path("..", "output", "rof_abs_prediction_error_by_learner.png"),
  device = "png", width = 10, height = 4.5)
ggplot(pred_user_q, aes(x = user_order, group = prediction_label, colour = prediction_label)) +
  facet_grid(~ course) +
  geom_ribbon(aes(ymin = q25, ymax = q75, fill = prediction_label), colour = NA, alpha = .15) +
  geom_line(aes(y = median), lwd = 1) +
    labs(x = "Learners",
       y = "Absolute SoF prediction error",
       colour = "Prediction\nmethod",
       fill = "Prediction\nmethod") +
  scale_colour_manual(values = condition_colours) +
  scale_fill_manual(values = condition_colours) +
  theme(axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    panel.grid.major.x = element_blank()) +
  NULL

ggsave(file.path("..", "output", "rof_abs_prediction_error_by_learner_condensed.png"),
  device = "png", width = 10, height = 4.5)

By fact

fact_freq <- pred_both[, .N, by = .(course, prediction_label, fact_id)]

pred_fact_freq <- pred_both[fact_freq[N > 50], on = .(course, prediction_label, fact_id)]

pred_fact_q <- pred_fact_freq[, .(stat = c("whisker_low", "q25", "median", "q75", "whisker_high"),
                                  value = boxplot.stats(prediction_error, do.conf = FALSE, do.out = FALSE)$stats), by = .(course, prediction_label, fact_id)]

pred_fact_q <- pivot_wider(pred_fact_q, names_from = "stat", values_from = "value")

pred_fact_q <- pred_fact_q %>%
  arrange(course, prediction_label, median) %>%
  group_by(course, prediction_label) %>%
  mutate(fact_order = (1:n())/n())
ggplot(pred_fact_q, aes(x = fact_order)) +
  facet_grid(course ~ prediction_label) +
  geom_ribbon(aes(ymin = whisker_low, ymax = whisker_high, fill = course), alpha = .3) +
  geom_ribbon(aes(ymin = q25, ymax = q75, fill = course), alpha = .5) +
  geom_line(aes(y = median), lwd = 1) +
  geom_hline(data = NULL, yintercept = 0, lty = 3) +
    labs(x = "Facts",
       y = "SoF prediction error\n(predicted - observed)") +
  scale_fill_manual(values = dataset_colours) +
  guides(fill = "none") +
  theme(axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    panel.grid.major.x = element_blank()) +
  NULL

ggsave(file.path("..", "output", "rof_prediction_error_by_fact.png"),
  device = "png", width = 10, height = 4.5)

Absolute error:

pred_fact_q <- pred_fact_freq[, .(stat = c("whisker_low", "q25", "median", "q75", "whisker_high"),
                                  value = boxplot.stats(abs_prediction_error, do.conf = FALSE, do.out = FALSE)$stats), by = .(course, prediction_label, fact_id)]

pred_fact_q <- pivot_wider(pred_fact_q, names_from = "stat", values_from = "value")

pred_fact_q <- pred_fact_q %>%
  arrange(course, prediction_label, median) %>%
  group_by(course, prediction_label) %>%
  mutate(fact_order = (1:n())/n())

ggplot(pred_fact_q, aes(x = fact_order)) +
  facet_grid(course ~ prediction_label) +
  geom_ribbon(aes(ymin = whisker_low, ymax = whisker_high, fill = course), alpha = .3) +
  geom_ribbon(aes(ymin = q25, ymax = q75, fill = course), alpha = .5) +
  geom_line(aes(y = median), lwd = 1) +
    labs(x = "Facts",
       y = "Absolute SoF prediction error") +
  scale_fill_manual(values = dataset_colours) +
  guides(fill = "none") +
  theme(axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    panel.grid.major.x = element_blank()) +
  NULL

ggsave(file.path("..", "output", "rof_abs_prediction_error_by_fact.png"),
  device = "png", width = 10, height = 4.5)
ggplot(pred_fact_q, aes(x = fact_order, group = prediction_label, colour = prediction_label)) +
  facet_grid(~ course) +
  geom_ribbon(aes(ymin = q25, ymax = q75, fill = prediction_label), colour = NA, alpha = .15) +
  geom_line(aes(y = median), lwd = 1) +
    labs(x = "Facts",
       y = "Absolute SoF prediction error",
       colour = "Prediction\nmethod",
       fill = "Prediction\nmethod") +
  scale_colour_manual(values = condition_colours) +
  scale_fill_manual(values = condition_colours) +
  theme(axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    panel.grid.major.x = element_blank()) +
  NULL

ggsave(file.path("..", "output", "rof_abs_prediction_error_by_fact_condensed.png"),
  device = "png", width = 10, height = 4.5)
ggplot(pred_both, aes(x = abs_prediction_error, colour = prediction_label, fill = prediction_label)) +
  facet_grid(~ course) +
  geom_density(alpha = .1) +
      labs(x = "Absolute SoF prediction error",
       y = "Density",
       colour = "Prediction\nmethod",
       fill = "Prediction\nmethod") +
  scale_colour_manual(values = condition_colours) +
  scale_fill_manual(values = condition_colours) +
  coord_cartesian(ylim = c(0, 100), xlim = c(0, .25)) +
  NULL

ggsave(file.path("..", "output", "rof_abs_prediction_error_density.png"),
  device = "png", width = 10, height = 4.5)

Session info

sessionInfo()
R version 4.3.1 (2023-06-16)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Sonoma 14.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/Amsterdam
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] fstcore_0.9.14    dplyr_1.1.3       emmeans_1.8.9     multcomp_1.4-25  
 [5] TH.data_1.1-2     MASS_7.3-60       survival_3.5-7    mvtnorm_1.2-3    
 [9] lmerTest_3.1-3    lme4_1.1-34       Matrix_1.6-1.1    wesanderson_0.3.6
[13] ggplot2_3.4.3     stringr_1.5.0     furrr_0.3.1       future_1.33.0    
[17] purrr_1.0.2       tidyr_1.3.0       data.table_1.14.8 fst_0.9.8        

loaded via a namespace (and not attached):
 [1] gtable_0.3.4        xfun_0.40           bslib_0.5.1        
 [4] lattice_0.21-9      numDeriv_2016.8-1.1 vctrs_0.6.3        
 [7] tools_4.3.1         generics_0.1.3      parallel_4.3.1     
[10] sandwich_3.0-2      tibble_3.2.1        fansi_1.0.4        
[13] pkgconfig_2.0.3     lifecycle_1.0.3     farver_2.1.1       
[16] compiler_4.3.1      textshaping_0.3.7   munsell_0.5.0      
[19] codetools_0.2-19    htmltools_0.5.6     sass_0.4.7         
[22] yaml_2.3.7          crayon_1.5.2        pillar_1.9.0       
[25] nloptr_2.0.3        jquerylib_0.1.4     cachem_1.0.8       
[28] boot_1.3-28.1       nlme_3.1-163        parallelly_1.36.0  
[31] tidyselect_1.2.0    digest_0.6.33       stringi_1.7.12     
[34] listenv_0.9.0       labeling_0.4.3      splines_4.3.1      
[37] fastmap_1.1.1       grid_4.3.1          colorspace_2.1-0   
[40] cli_3.6.1           magrittr_2.0.3      utf8_1.2.3         
[43] broom_1.0.5         withr_2.5.1         backports_1.4.1    
[46] scales_1.2.1        estimability_1.4.1  rmarkdown_2.25     
[49] globals_0.16.2      ggsignif_0.6.4      ragg_1.2.7         
[52] zoo_1.8-12          coda_0.19-4         evaluate_0.22      
[55] knitr_1.44          mgcv_1.9-0          rlang_1.1.1        
[58] Rcpp_1.0.11         xtable_1.8-4        glue_1.6.2         
[61] rstudioapi_0.15.0   minqa_1.2.6         jsonlite_1.8.7     
[64] R6_2.5.1            systemfonts_1.0.4  
LS0tCnRpdGxlOiAiRXZhbHVhdGUgcmF0ZSBvZiBmb3JnZXR0aW5nIHByZWRpY3Rpb25zIgphdXRob3I6ICJNYWFydGVuIHZhbiBkZXIgVmVsZGUiCmRhdGU6ICJMYXN0IHVwZGF0ZWQ6IGByIFN5cy5EYXRlKClgIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIHNtYXJ0OiBubwogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogeWVzCiAgZ2l0aHViX2RvY3VtZW50OgogICAgdG9jOiB5ZXMKZWRpdG9yX29wdGlvbnM6IAogIGNodW5rX291dHB1dF90eXBlOiBpbmxpbmUKLS0tCgoKIyBPdmVydmlldwoKVGhpcyBub3RlYm9vayBldmFsdWF0ZXMgdGhlIHByZWRpY3RlZCByYXRlcyBvZiBmb3JnZXR0aW5nLgpUaGUgcHJlZGljdGlvbnMgYXJlIGNvbXBhcmVkIHRvIHRoZSB2YWx1ZSBkZXJpdmVkIGF0IHRoZSBlbmQgb2YgZWFjaCBsZWFybmluZyBzZXNzaW9uIHRvIGRldGVybWluZSB0aGVpciBhY2N1cmFjeS4KCgojIFNldHVwCgpgYGB7cn0KbGlicmFyeShmc3QpCmxpYnJhcnkoZGF0YS50YWJsZSkKbGlicmFyeSh0aWR5cikKbGlicmFyeShwdXJycikKbGlicmFyeShmdXJycikKbGlicmFyeShzdHJpbmdyKQpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkod2VzYW5kZXJzb24pCmxpYnJhcnkobG1lNCkKbGlicmFyeShsbWVyVGVzdCkKbGlicmFyeShtdWx0Y29tcCkKbGlicmFyeShlbW1lYW5zKQpgYGAKCmBgYHtyfQpzb3VyY2UoZmlsZS5wYXRoKCIuLiIsICJzY3JpcHRzIiwgIjk5X3NsaW1zdGFtcGVuX21vZGVsX2Z1bnMuUiIpKQpgYGAKCmBgYHtyfQpmdXR1cmU6OnBsYW4oIm11bHRpc2Vzc2lvbiIsIHdvcmtlcnMgPSA2KSAjIFNldCB0byBkZXNpcmVkIG51bWJlciBvZiBjb3JlcwpgYGAKCmBgYHtyfQp0aGVtZV9zZXQodGhlbWVfbGlnaHQoYmFzZV9zaXplID0gMTQpICsKICAgICAgICAgICAgdGhlbWUoc3RyaXAudGV4dCA9IGVsZW1lbnRfdGV4dChjb2xvdXIgPSAiYmxhY2siKSkpCgpjb25kaXRpb25fY29sb3VycyA8LSB3ZXNfcGFsZXR0ZSgiRGFyamVlbGluZzEiLCBuID0gNSkKY29uZGl0aW9uX2NvbG91cnNbYygyLCA0LCA1KV0gPC0gY29uZGl0aW9uX2NvbG91cnNbYyg0LCA1LCAyKV0KCmRhdGFzZXRfY29sb3VycyA8LSB3ZXNfcGFsZXR0ZSgiRGFyamVlbGluZzIiLCBuID0gNSlbYygyLCAzKV0KYGBgCgoKIyMgSGVscGVyIGZ1bmN0aW9ucwoKYGBge3J9CmxvYWRfcHJlZGljdGlvbnMgPC0gZnVuY3Rpb24oY291cnNlKSB7CiAgCiAgcHJlZF91c2VyIDwtIHJlYWRfZnN0KGZpbGUucGF0aCgiLi4iLCAiZGF0YSIsICJwcmVkaWN0aW9ucyIsIHBhc3RlMCgicHJlZF92X29ic191c2VyXyIsIHN0cl9yZXBsYWNlX2FsbChjb3Vyc2UsICIgIiwgIl8iKSwgIi5mc3QiKSkpCiAgcHJlZF9mYWN0IDwtIHJlYWRfZnN0KGZpbGUucGF0aCgiLi4iLCAiZGF0YSIsICJwcmVkaWN0aW9ucyIsIHBhc3RlMCgicHJlZF92X29ic19mYWN0XyIsIHN0cl9yZXBsYWNlX2FsbChjb3Vyc2UsICIgIiwgIl8iKSwgIi5mc3QiKSkpCiAgcHJlZF9mYWN0X3VzZXIgPC0gcmVhZF9mc3QoZmlsZS5wYXRoKCIuLiIsICJkYXRhIiwgInByZWRpY3Rpb25zIiwgcGFzdGUwKCJwcmVkX2ZhY3RfYW5kX3VzZXJfIiwgc3RyX3JlcGxhY2VfYWxsKGNvdXJzZSwgIiAiLCAiXyIpLCAiLmZzdCIpKSkKICBzZXREVChwcmVkX3VzZXIpCiAgc2V0RFQocHJlZF9mYWN0KQogIHNldERUKHByZWRfZmFjdF91c2VyKQogIAogIHByZWRfZG9tYWluIDwtIG1lYW4odW5pcXVlKHByZWRfZmFjdCwgYnkgPSBjKCJmYWN0X2lkIikpJHByZWRfZmFjdCkKICBwcmVkX2RlZmF1bHQgPC0gMC4zCiAgCiAgIyBDb21iaW5lCiAgcHJlZF9hbGwgPC0gbWVyZ2UocHJlZF91c2VyLCBwcmVkX2ZhY3QsIGJ5ID0gYygidXNlcl9pZCIsICJmYWN0X2lkIiwgImFscGhhIiwgIm5fcmVwcyIpLCBhbGwgPSBUUlVFKQogIHByZWRfYWxsIDwtIG1lcmdlKHByZWRfYWxsLCBwcmVkX2ZhY3RfdXNlciwgYnkgPSBjKCJ1c2VyX2lkIiwgImZhY3RfaWQiLCAiYWxwaGEiKSwgYWxsID0gVFJVRSkKICBwcmVkX2FsbFssIHByZWRfZGVmYXVsdCA6PSBwcmVkX2RlZmF1bHRdCiAgcHJlZF9hbGxbLCBwcmVkX2RvbWFpbiA6PSBwcmVkX2RvbWFpbl0KICAKICBwcmVkX29ic19sb25nIDwtIHBpdm90X2xvbmdlcihwcmVkX2FsbCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29scyA9IHByZWRfdXNlcjpwcmVkX2RvbWFpbiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuYW1lc190byA9ICJwcmVkaWN0aW9uX3R5cGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5hbWVzX3ByZWZpeCA9ICJwcmVkXFxfIikKICAKICBzZXREVChwcmVkX29ic19sb25nKQogIAogICMgUmVtb3ZlIE5BIHByZWRpY3Rpb25zIGFuZCBwcmVkaWN0aW9ucyB3aXRob3V0IGNvcnJlc3BvbmRpbmcgb2JzZXJ2YXRpb25zCiAgcHJlZF9vYnNfbG9uZyA8LSBwcmVkX29ic19sb25nWyFpcy5uYSh2YWx1ZSldCiAgcHJlZF9vYnNfbG9uZyA8LSBwcmVkX29ic19sb25nWyFpcy5uYShhbHBoYSldCiAgCiAgIyBSZW1vdmUgZHVwbGljYXRlcwogIHByZWRfb2JzX2xvbmcgPC0gdW5pcXVlKHByZWRfb2JzX2xvbmcpCiAgCiAgIyBTZXQgcHJvcGVyIGxhYmVscwogIGNvbmRpdGlvbl9sYWJlbHMgPC0gZGF0YS50YWJsZShwcmVkaWN0aW9uX3R5cGUgPSBjKCJkZWZhdWx0IiwgImRvbWFpbiIsICJmYWN0IiwgInVzZXIiLCAiZmFjdF91c2VyIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHByZWRpY3Rpb25fbGFiZWwgPSBmYWN0b3IoYygiRGVmYXVsdCIsICJEb21haW4iLCAiRmFjdCIsICJMZWFybmVyIiwgIkZhY3QgJiBMZWFybmVyIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzID0gYygiRGVmYXVsdCIsICJEb21haW4iLCAiRmFjdCIsICJMZWFybmVyIiwgIkZhY3QgJiBMZWFybmVyIikpKQogIHByZWRfb2JzX2xvbmcgPC0gcHJlZF9vYnNfbG9uZ1tjb25kaXRpb25fbGFiZWxzLCBvbiA9IC4ocHJlZGljdGlvbl90eXBlKV0KICAKICByZXR1cm4ocHJlZF9vYnNfbG9uZykKfQpgYGAKCgoKIyBSYXRlIG9mIGZvcmdldHRpbmcKCiMjIFByZWRpY3RlZCByYXRlIG9mIGZvcmdldHRpbmcKYGBge3J9CnByZWRfZ2wgPC0gbG9hZF9wcmVkaWN0aW9ucygiR3JhbmRlcyBMaWduZXMiKQpwcmVkX3NzIDwtIGxvYWRfcHJlZGljdGlvbnMoIlN0ZXBwaW5nIFN0b25lcyIpCgpwcmVkX2JvdGggPC0gcmJpbmQocHJlZF9nbFssIGNvdXJzZSA6PSAiRnJlbmNoIl0sCiAgICAgICAgICAgICAgICAgICBwcmVkX3NzWywgY291cnNlIDo9ICJFbmdsaXNoIl0pCmBgYAoKIyMjIERpc3RyaWJ1dGlvbiBvZiBwcmVkaWN0aW9ucwpgYGB7ciB9CnBfcm9mX2Rpc3QgPC0gZ2dwbG90KHByZWRfYm90aCwgYWVzKHggPSB2YWx1ZSwgZmlsbCA9IHByZWRpY3Rpb25fbGFiZWwpKSArCiAgZmFjZXRfZ3JpZChwcmVkaWN0aW9uX2xhYmVsIH4gY291cnNlLCBzY2FsZXMgPSAiZnJlZV95IikgKwogIGdlb21faGlzdG9ncmFtKGFlcyh5ID0gLi5kZW5zaXR5Li4pLCBiaW53aWR0aCA9IC4wMSkgKwogIGd1aWRlcyhmaWxsID0gIm5vbmUiKSArCiAgbGFicyh4ID0gIlByZWRpY3RlZCBTcGVlZCBvZiBGb3JnZXR0aW5nIiwKICAgICAgIHkgPSAiRGVuc2l0eSIpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjb25kaXRpb25fY29sb3VycykKCnBfcm9mX2Rpc3QKCmdnc2F2ZShwbG90ID0gcF9yb2ZfZGlzdCwgZmlsZS5wYXRoKCIuLiIsICJvdXRwdXQiLCAicm9mX3ByZWRpY3Rpb25zX2Rpc3RyaWJ1dGlvbi5wbmciKSwKICAgICAgIGRldmljZSA9ICJwbmciLCB3aWR0aCA9IDUsIGhlaWdodCA9IDcuNSkKYGBgCgojIyMgUHJlZGljdGVkIHZzLiBvYnNlcnZlZCB2YWx1ZXMKCldlIGNvbXBhcmUgdGhlIHByZWRpY3RlZCByYXRlIG9mIGZvcmdldHRpbmcgdG8gdGhlICJvYnNlcnZlZCIgcmF0ZSBvZiBmb3JnZXR0aW5nLCBpLmUuLCB0aGUgcmF0ZSBvZiBmb3JnZXR0aW5nIHRoYXQgd2FzIGVzdGltYXRlZCBhdCB0aGUgZW5kIG9mIHRoZSBsZWFybmluZyBzZXF1ZW5jZS4KClRvIGFzc2VzcyB0aGUgYWNjdXJhY3kgb2YgcHJlZGljdGlvbnMsIHdlIGNvbXB1dGUgdGhlIG1lYW4gYWJzb2x1dGUgZXJyb3IgKE1BRSkgYXMgYW4gYWdncmVnYXRlIHN0YXRpc3RpYywgYXMgd2VsbCBhcyB0aGUgYWJzb2x1dGUgZXJyb3IgKEFFKSBvZiBlYWNoIGluZGl2aWR1YWwgcHJlZGljdGlvbi4KCmBgYHtyfQpwcmVkX21hZSA8LSBwcmVkX2JvdGhbLCAuKG1hZSA9IG1lYW4oYWJzKGFscGhhIC0gdmFsdWUpKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBhZV9zZSA9IHNkKGFicyhhbHBoYSAtIHZhbHVlKSkvc3FydCguTikpLCAKICAgICAgICAgICAgICAgICAgICAgIGJ5ID0gLihjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwpXQoKICAKbl9vYnMgPC0gcHJlZF9ib3RoWywgLk4sIGJ5ID0gLihjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwpXQpgYGAKClBsb3QgcHJlZGljdGVkIHZzLiBvYnNlcnZlZCB2YWx1ZXM6CmBgYHtyIH0Kcm9mX21pbiA8LSAwCnJvZl9tYXggPC0gMQpyb2ZfYnJlYWtzIDwtIHNlcSgwLjEsIDAuOSwgYnkgPSAuMikKCnBfcm9mX3ByZWRfdl9vYnMgPC0gZ2dwbG90KHByZWRfYm90aCwKICAgICAgICAgICAgICAgICAgICAgICAgIGFlcyh4ID0gdmFsdWUsIHkgPSBhbHBoYSwgY29sb3VyID0gcHJlZGljdGlvbl9sYWJlbCkpICsKICAgIGZhY2V0X2dyaWQoY291cnNlIH4gcHJlZGljdGlvbl9sYWJlbCkgKwogICAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMC4zLCBsdHkgPSAyKSArCiAgICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAwLjMsIGx0eSA9IDIpICsKICAgIGdlb21fYWJsaW5lKHNsb3BlID0gMSwgaW50ZXJjZXB0ID0gMCwgbHR5ID0gMywgYWxwaGEgPSAwLjc1KSArCiAgICBnZW9tX3BvaW50KGFscGhhID0gLjEsIHNpemUgPSAuMSwgcGNoID0gIi4iKSArCiAgICBnZW9tX3Ntb290aChtZXRob2QgPSAibG0iLCBmb3JtdWxhID0geSB+IHgsIGNvbG91ciA9ICJibGFjayIpICsKICBnZW9tX2xhYmVsKGRhdGEgPSBwcmVkX21hZSwKICAgICAgICAgICAgIGFlcyhsYWJlbCA9IHBhc3RlKCJNQUUgPSIsIGZvcm1hdEMobWFlLCBkaWdpdHMgPSAzLCBmbGFnID0gIiMiKSkpLAogICAgICAgICAgICAgeCA9IHJvZl9tYXgsIHkgPSByb2ZfbWluLAogICAgICAgICAgICAgaGp1c3QgPSAxLCBjb2xvdXIgPSAiTkEiLCBzaXplID0gMywKICAgICAgICAgICAgIGFscGhhID0gLjksCiAgICAgICAgICAgICBsYWJlbC5zaXplID0gTkEpICsKICBnZW9tX3RleHQoZGF0YSA9IHByZWRfbWFlLAogICAgICAgICAgICBhZXMobGFiZWwgPSBwYXN0ZSgiTUFFID0iLCBmb3JtYXRDKG1hZSwgZGlnaXRzID0gMywgZmxhZyA9ICIjIikpKSwKICAgICAgICAgICAgeCA9IHJvZl9tYXgsIHkgPSByb2ZfbWluLAogICAgICAgICAgICBoanVzdCA9IDEsIGNvbG91ciA9ICJibGFjayIsIHNpemUgPSAzKSArCiAgZ2VvbV9sYWJlbChkYXRhID0gbl9vYnMsCiAgICAgICAgICAgICBhZXMobGFiZWwgPSBwYXN0ZSgibiA9Iiwgc2NhbGVzOjpjb21tYShOKSkpLAogICAgICAgICAgICAgeCA9IHJvZl9tYXgsCiAgICAgICAgICAgICB5ID0gcm9mX21heCwKICAgICAgICAgICAgIGhqdXN0ID0gMSwgY29sb3VyID0gIk5BIiwgc2l6ZSA9IDMsCiAgICAgICAgICAgICBhbHBoYSA9IC45LAogICAgICAgICAgICAgbGFiZWwuc2l6ZSA9IE5BKSArCiAgZ2VvbV90ZXh0KGRhdGEgPSBuX29icywKICAgICAgICAgICAgYWVzKGxhYmVsID0gcGFzdGUoIm4gPSIsIHNjYWxlczo6Y29tbWEoTikpKSwKICAgICAgICAgICAgeCA9IHJvZl9tYXgsCiAgICAgICAgICAgIHkgPSByb2ZfbWF4LAogICAgICAgICAgICBoanVzdCA9IDEsIGNvbG91ciA9ICJibGFjayIsIHNpemUgPSAzKSArCiAgZ3VpZGVzKGNvbG91ciA9ICJub25lIikgKwogIGxhYnMoeCA9ICJQcmVkaWN0ZWQgU3BlZWQgb2YgRm9yZ2V0dGluZyDOsSIsCiAgICAgICB5ID0gIk9ic2VydmVkIFNwZWVkIG9mIEZvcmdldHRpbmcgzrEiKSArCiAgY29vcmRfZml4ZWQocmF0aW8gPSAxLCB4bGltID0gYyhyb2ZfbWluLCByb2ZfbWF4KSwgeWxpbSA9IGMocm9mX21pbiwgcm9mX21heCkpICsKICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzID0gcm9mX2JyZWFrcykgKwogIHNjYWxlX3lfY29udGludW91cyhicmVha3MgPSByb2ZfYnJlYWtzKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjb25kaXRpb25fY29sb3VycykKCnBfcm9mX3ByZWRfdl9vYnMKCmdnc2F2ZShwbG90ID0gcF9yb2ZfcHJlZF92X29icywgZmlsZS5wYXRoKCIuLiIsICJvdXRwdXQiLCAicm9mX3ByZWRpY3RlZF92c19vYnNlcnZlZC5wbmciKSwKICBkZXZpY2UgPSAicG5nIiwgd2lkdGggPSAxMCwgaGVpZ2h0ID0gNC41KQoKcm0ocF9yb2ZfcHJlZF92X29icykKYGBgCgojIyMgUHJlZGljdGlvbiBlcnJvcgoKQ2FsY3VsYXRlIHByZWRpY3Rpb24gZXJyb3I6CmBgYHtyfQpwcmVkX2JvdGhbLCBwcmVkaWN0aW9uX2Vycm9yIDo9IHZhbHVlIC0gYWxwaGFdCmBgYAoKRGlzdHJpYnV0aW9uIG9mIHByZWRpY3Rpb24gZXJyb3I6CmBgYHtyIH0KcF9yb2ZfcHJlZF9lcnJvciA8LSBnZ3Bsb3QocHJlZF9ib3RoLCBhZXMoeCA9IHByZWRpY3Rpb25fZXJyb3IsIGZpbGwgPSBwcmVkaWN0aW9uX2xhYmVsKSkgKwogIGZhY2V0X2dyaWQocHJlZGljdGlvbl9sYWJlbCB+IGNvdXJzZSwgc2NhbGVzID0gImZyZWVfeSIpICsKICBnZW9tX2hpc3RvZ3JhbShhZXMoeSA9IC4uZGVuc2l0eS4uKSwgYmlud2lkdGggPSAuMDEpICsKICBndWlkZXMoZmlsbCA9ICJub25lIikgKwogIGxhYnMoeCA9ICJTb0YgcHJlZGljdGlvbiBlcnJvciAocHJlZGljdGVkIC0gb2JzZXJ2ZWQpIiwKICAgICAgIHkgPSAiRGVuc2l0eSIpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjb25kaXRpb25fY29sb3VycykKCnBfcm9mX3ByZWRfZXJyb3IKCmdnc2F2ZShwbG90ID0gcF9yb2ZfcHJlZF9lcnJvciwgZmlsZS5wYXRoKCIuLiIsICJvdXRwdXQiLCAicm9mX3ByZWRpY3Rpb25fZXJyb3IucG5nIiksCiAgICAgICBkZXZpY2UgPSAicG5nIiwgd2lkdGggPSA1LCBoZWlnaHQgPSA3LjUpCmBgYAoKCiMjIyMgQWJzb2x1dGUgcHJlZGljdGlvbiBlcnJvcgoKVG8gY29tcGFyZSB0aGUgbWFnbml0dWRlIG9mIHByZWRpY3Rpb24gZXJyb3JzIGJldHdlZW4gcHJlZGljdGlvbiBtZXRob2RzLCB3ZSBsb29rIGF0IGFic29sdXRlIHByZWRpY3Rpb24gZXJyb3IuCgpgYGB7cn0KcHJlZF9ib3RoWywgYWJzX3ByZWRpY3Rpb25fZXJyb3IgOj0gYWJzKHByZWRpY3Rpb25fZXJyb3IpXQpgYGAKCkRpc3RyaWJ1dGlvbiBvZiBhYnNvbHV0ZSBwcmVkaWN0aW9uIGVycm9yOgpgYGB7ciB9CnBfcm9mX2Fic19wcmVkX2Vycm9yIDwtIGdncGxvdChwcmVkX2JvdGgsIGFlcyh4ID0gYWJzX3ByZWRpY3Rpb25fZXJyb3IsIGZpbGwgPSBwcmVkaWN0aW9uX2xhYmVsKSkgKwogIGZhY2V0X2dyaWQocHJlZGljdGlvbl9sYWJlbCB+IGNvdXJzZSwgc2NhbGVzID0gImZyZWVfeSIpICsKICBnZW9tX2hpc3RvZ3JhbShhZXMoeSA9IC4uZGVuc2l0eS4uKSwgYmlud2lkdGggPSAuMDEpICsKICBndWlkZXMoZmlsbCA9ICJub25lIikgKwogIGxhYnMoeCA9ICJBYnNvbHV0ZSBTb0YgcHJlZGljdGlvbiBlcnJvciIsCiAgICAgICB5ID0gIkRlbnNpdHkiKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gY29uZGl0aW9uX2NvbG91cnMpCgpwX3JvZl9hYnNfcHJlZF9lcnJvcgoKZ2dzYXZlKHBsb3QgPSBwX3JvZl9hYnNfcHJlZF9lcnJvciwgZmlsZS5wYXRoKCIuLiIsICJvdXRwdXQiLCAicm9mX2Fic29sdXRlX3ByZWRpY3Rpb25fZXJyb3IucG5nIiksCiAgICAgICBkZXZpY2UgPSAicG5nIiwgd2lkdGggPSA1LCBoZWlnaHQgPSA3LjUpCmBgYAoKYGBge3J9CnByZWRfZXJyb3Jfc3VtbWFyaXNlZCA8LSBwcmVkX2JvdGhbLCAuKGVycm9yX21lYW4gPSBtZWFuKGFic19wcmVkaWN0aW9uX2Vycm9yKSwgZXJyb3Jfc2UgPSBzZChhYnNfcHJlZGljdGlvbl9lcnJvcikvc3FydCguTikpLCBieSA9IC4oY291cnNlLCBwcmVkaWN0aW9uX2xhYmVsKV0KCmdncGxvdChwcmVkX2Vycm9yX3N1bW1hcmlzZWQsIGFlcyh4ID0gcHJlZGljdGlvbl9sYWJlbCwgeSA9IGVycm9yX21lYW4sIGNvbG91ciA9IGNvdXJzZSkpICsKICBnZW9tX2JveHBsb3QoZGF0YSA9IHByZWRfYm90aCwKICAgICAgICAgICAgICAgYWVzKHkgPSBhYnNfcHJlZGljdGlvbl9lcnJvciwgZ3JvdXAgPSBpbnRlcmFjdGlvbihjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwpKSwKICAgICAgICAgICAgICAgY29sb3VyID0gImdyZXk3MCIsCiAgICAgICAgICAgICAgIHdpZHRoID0gLjI1LAogICAgICAgICAgICAgICBvdXRsaWVyLnNoYXBlID0gTkEsCiAgICAgICAgICAgICAgIHBvc2l0aW9uID0gcG9zaXRpb25fZG9kZ2Uod2lkdGggPSAuNSkpICsKICBnZW9tX2Vycm9yYmFyKGFlcyh5bWluID0gZXJyb3JfbWVhbiAtIGVycm9yX3NlLCB5bWF4ID0gZXJyb3JfbWVhbiArIGVycm9yX3NlKSwgd2lkdGggPSAwLCBwb3NpdGlvbiA9IHBvc2l0aW9uX2RvZGdlKHdpZHRoID0gLjUpKSArCiAgZ2VvbV9wb2ludChwb3NpdGlvbiA9IHBvc2l0aW9uX2RvZGdlKHdpZHRoID0gLjUpKSArCiAgY29vcmRfY2FydGVzaWFuKHlsaW0gPSBjKDAsIDAuMTc1KSkgKwogIGxhYnMoeCA9ICJNZXRob2QiLAogICAgICAgeSA9ICJBYnNvbHV0ZSBTb0YgcHJlZGljdGlvbiBlcnJvciIsCiAgICAgICBjb2xvdXIgPSAiQ291cnNlIikKYGBgCgoKCgpGaXQgYSByZWdyZXNzaW9uIG1vZGVsIG9uIGFic29sdXRlIHByZWRpY3Rpb24gZXJyb3IuClRoZSB3aG9sZSBkYXRhIHNldCBpcyB0b28gYmlnIHRvIGZpdCBpbiBhIHJlYXNvbmFibGUgdGltZSwgc28gd2UgZml0IHRoZSBtb2RlbCB0byBhIHJhbmRvbSBzdWJzZXQgb2YgMU0gcHJlZGljdGlvbnMgKHdoaWNoIGFscmVhZHkgdGFrZXMgfjI0aHJzKS4gCgojIyMjIyBHcmFuZGVzIExpZ25lcwpgYGB7cn0KbV9yb2ZfcHJlZF9lcnJvcl9nbF9maWxlIDwtIGZpbGUucGF0aCgiLi4iLCAiZGF0YSIsICJtb2RlbF9maXRzIiwgIm1fcHJlZF9lcnJvcl9HcmFuZGVzX0xpZ25lc18xZTYucmRhIikKCmlmIChmaWxlLmV4aXN0cyhtX3JvZl9wcmVkX2Vycm9yX2dsX2ZpbGUpKSB7CiAgbG9hZChtX3JvZl9wcmVkX2Vycm9yX2dsX2ZpbGUpCn0gZWxzZSB7CiAgCiAgcHJlZF9nbF9yZWcgPC0gKAogICAgcHJlZF9ib3RoCiAgICBbY291cnNlID09ICJGcmVuY2giXQogICAgW3NhbXBsZSguTiwgMWU2KSwgLihwcmVkaWN0aW9uX2xhYmVsLCBhYnNfcHJlZGljdGlvbl9lcnJvciwgdXNlcl9pZCwgZmFjdF9pZCldCiAgKQogIAogIG1fcm9mX3ByZWRfZXJyb3JfZ2wgPC0gbG1lcihhYnNfcHJlZGljdGlvbl9lcnJvciB+IHByZWRpY3Rpb25fbGFiZWwgKyAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAoMSB8IHVzZXJfaWQpICsgKDEgfCBmYWN0X2lkKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IHByZWRfZ2xfcmVnKQogIAogIHNhdmUobV9yb2ZfcHJlZF9lcnJvcl9nbCwgZmlsZSA9IG1fcm9mX3ByZWRfZXJyb3JfZ2xfZmlsZSkKfQoKc3VtbWFyeShtX3JvZl9wcmVkX2Vycm9yX2dsKQpgYGAKCkNvbXBhcmUgZGlmZmVyZW50IHByZWRpY3Rpb24gdHlwZXMgdG8gZWFjaCBvdGhlcjoKYGBge3J9Cmh0X2dsIDwtIGdsaHQobV9yb2ZfcHJlZF9lcnJvcl9nbCwgbGluZmN0ID0gbWNwKHByZWRpY3Rpb25fbGFiZWwgPSAiVHVrZXkiKSkKc3VtbWFyeShodF9nbCkKYGBgClVzZSBgZW1tZWFuc2AgdG8gZXN0aW1hdGUgdGhlIHN0YW5kYXJkaXNlZCBlZmZlY3Qgc2l6ZSBmb3IgZWFjaCBjb250cmFzdDoKYGBge3J9CmVtbV9nbCA8LSBlbW1lYW5zKG1fcm9mX3ByZWRfZXJyb3JfZ2wsICJwcmVkaWN0aW9uX2xhYmVsIiwgbG1lci5kZiA9ICJhc3ltcHRvdGljIikKIyBTaWdtYSBjYWxjdWxhdGlvbiBmb2xsb3dzIHRoZSBleGFtcGxlIGluIHRoZSBlZmZfc2l6ZSBkb2N1bWVudGF0aW9uIChzZWUgYWxzbyBXZXN0ZmFsbCBldCBhbC4sIDIwMTQpCnZjX2dsIDwtIGFzLmRhdGEuZnJhbWUoVmFyQ29ycihtX3JvZl9wcmVkX2Vycm9yX2dsKSkgCnNpZ21hX2dsIDwtIHNxcnQoc3VtKHZjX2dsJHZjb3YpKQplZmZfc2l6ZV9nbCA8LSBlZmZfc2l6ZShlbW1fZ2wsIHNpZ21hID0gc2lnbWFfZ2wsIGVkZiA9IEluZikgIyBDaG9pY2Ugb2YgZWRmIGRvZXMgbm90IGFmZmVjdCBlZmZlY3Qgc2l6ZSwgb25seSB0aGUgU0UKZWZmX3NpemVfZ2wKYGBgCgoKCkluc3BlY3QgdGhlIG1vZGVsJ3MgcmVzaWR1YWxzOgpgYGB7cn0KcXFub3JtKHJlc2lkKG1fcm9mX3ByZWRfZXJyb3JfZ2wpKQpxcWxpbmUocmVzaWQobV9yb2ZfcHJlZF9lcnJvcl9nbCksIGNvbCA9ICJyZWQiKQpgYGAKCmBgYHtyfQpwbG90KG1fcm9mX3ByZWRfZXJyb3JfZ2wpCmBgYAoKClRoZSBRUSBwbG90IGluZGljYXRlcyBxdWl0ZSBhIHN0cm9uZyBza2V3LCB3aGljaCBpcyBub3Qgc3VycHJpc2luZywgZ2l2ZW4gdGhhdCB0aGUgZGlzdHJpYnV0aW9uIG9mIGFic29sdXRlIGVycm9yIGlzIGJvdW5kZWQgYnkgemVybyBvbiB0aGUgbGVmdCBidXQgdW5ib3VuZGVkIG9uIHRoZSByaWdodC4KQXNzdW1pbmcgYSBHYW1tYSBkaXN0cmlidXRpb24gbWF5IGJlIGJldHRlciwgYnV0IG1vZGVscyB0aGF0IHVzZSBhIEdhbW1hIGRpc3RyaWJ1dGlvbiBkbyBub3QgY29udmVyZ2UgaGVyZS4KVGhlIExNRVIgYWxzbyBnaXZlcyBhIHN1ZmZpY2llbnRseSBhY2N1cmF0ZSBlc3RpbWF0ZSBvZiB0aGUgbWVhbnMuCgoKCiMjIyMjIFN0ZXBwaW5nIFN0b25lcwoKYGBge3J9Cm1fcm9mX3ByZWRfZXJyb3Jfc3NfZmlsZSA8LSBmaWxlLnBhdGgoIi4uIiwgImRhdGEiLCAibW9kZWxfZml0cyIsICJtX3ByZWRfZXJyb3JfU3RlcHBpbmdfU3RvbmVzXzFlNi5yZGEiKQoKaWYgKGZpbGUuZXhpc3RzKG1fcm9mX3ByZWRfZXJyb3Jfc3NfZmlsZSkpIHsKICBsb2FkKG1fcm9mX3ByZWRfZXJyb3Jfc3NfZmlsZSkKfSBlbHNlIHsKICAKICBwcmVkX3NzX3JlZyA8LSAoCiAgICBwcmVkX2JvdGgKICAgIFtjb3Vyc2UgPT0gIkVuZ2xpc2giXQogICAgW3NhbXBsZSguTiwgMWU2KSwgLihwcmVkaWN0aW9uX2xhYmVsLCBhYnNfcHJlZGljdGlvbl9lcnJvciwgdXNlcl9pZCwgZmFjdF9pZCldCiAgKQogIAogIG1fcHJlZF9lcnJvciA8LSBsbWVyKGFic19wcmVkaWN0aW9uX2Vycm9yIH4gcHJlZGljdGlvbl9sYWJlbCArIAogICAgICAgICAgICAgICAgICAgICAgICAgKDEgfCB1c2VyX2lkKSArICgxIHwgZmFjdF9pZCksCiAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IHByZWRfc3NfcmVnKQogIAogIHNhdmUobV9wcmVkX2Vycm9yLCBmaWxlID0gbV9yb2ZfcHJlZF9lcnJvcl9zc19maWxlKQp9CgpzdW1tYXJ5KG1fcHJlZF9lcnJvcikKYGBgCgpDb21wYXJlIGRpZmZlcmVudCBwcmVkaWN0aW9uIHR5cGVzIHRvIGVhY2ggb3RoZXI6CmBgYHtyfQpodF9zcyA8LSBnbGh0KG1fcHJlZF9lcnJvciwgbGluZmN0ID0gbWNwKHByZWRpY3Rpb25fbGFiZWwgPSAiVHVrZXkiKSkKc3VtbWFyeShodF9zcykKYGBgCgpVc2UgYGVtbWVhbnNgIHRvIGVzdGltYXRlIHRoZSBzdGFuZGFyZGlzZWQgZWZmZWN0IHNpemUgZm9yIGVhY2ggY29udHJhc3Q6CmBgYHtyfQplbW1fc3MgPC0gZW1tZWFucyhtX3ByZWRfZXJyb3IsICJwcmVkaWN0aW9uX2xhYmVsIiwgbG1lci5kZiA9ICJhc3ltcHRvdGljIikKIyBTaWdtYSBjYWxjdWxhdGlvbiBmb2xsb3dzIHRoZSBleGFtcGxlIGluIHRoZSBlZmZfc2l6ZSBkb2N1bWVudGF0aW9uIChzZWUgYWxzbyBXZXN0ZmFsbCBldCBhbC4sIDIwMTQpCnZjX3NzIDwtIGFzLmRhdGEuZnJhbWUoVmFyQ29ycihtX3ByZWRfZXJyb3IpKSAKc2lnbWFfc3MgPC0gc3FydChzdW0odmNfc3MkdmNvdikpCmVmZl9zaXplX3NzIDwtIGVmZl9zaXplKGVtbV9zcywgc2lnbWEgPSBzaWdtYV9zcywgZWRmID0gSW5mKSAjIENob2ljZSBvZiBlZGYgZG9lcyBub3QgYWZmZWN0IGVmZmVjdCBzaXplLCBvbmx5IHRoZSBTRQplZmZfc2l6ZV9zcwpgYGAKCgpJbnNwZWN0IHRoZSBtb2RlbCdzIHJlc2lkdWFsczoKYGBge3J9CnFxbm9ybShyZXNpZChtX3ByZWRfZXJyb3IpKQpxcWxpbmUocmVzaWQobV9wcmVkX2Vycm9yKSwgY29sID0gInJlZCIpCmBgYAoKCiMjIyMjIENvbXBhcmlzb24KYGBge3J9Cmh0X2dsX3RpZHkgPC0gYnJvb206OnRpZHkoY29uZmludChodF9nbCkpCmh0X3NzX3RpZHkgPC0gYnJvb206OnRpZHkoY29uZmludChodF9zcykpCnNldERUKGh0X2dsX3RpZHkpCnNldERUKGh0X3NzX3RpZHkpCgpodF9ib3RoX3RpZHkgPC0gcmJpbmQoaHRfZ2xfdGlkeVssIGNvdXJzZSA6PSAiRnJlbmNoIl0sCiAgICAgICAgICAgICAgICAgICAgICBodF9zc190aWR5WywgY291cnNlIDo9ICJFbmdsaXNoIl0pCmBgYAoKYGBge3J9CnBfcm9mX3ByZWRfZXJyb3JfY29tcCA8LSBnZ3Bsb3QoaHRfYm90aF90aWR5LCBhZXMoeCA9IGNvbnRyYXN0LCB5ID0gZXN0aW1hdGUsIHltaW4gPSBjb25mLmxvdywgeW1heCA9IGNvbmYuaGlnaCwgY29sb3VyID0gY291cnNlKSkgKwogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDAsIGxpbmV0eXBlID0gIjExIiwgY29sb3VyID0gImdyZXk2MCIpICsKICBnZW9tX2Vycm9yYmFyKHdpZHRoID0gMC4xKSArIAogIGdlb21fcG9pbnQoKSArCiAgbGFicyh4ID0gIkxpbmVhciBoeXBvdGhlc2VzIiwKICAgICAgIHkgPSAiRXN0aW1hdGUiLAogICAgICAgY2FwdGlvbiA9ICJUdWtleSdzIHJhbmdlIHRlc3QuIEVycm9yIGJhcnMgc2hvdyA5NSUgZmFtaWx5LXdpc2UgY29uZmlkZW5jZSBsZXZlbC4iLAogICAgICAgY29sb3VyID0gIkNvdXJzZSIpICsKICAgIGNvb3JkX2ZsaXAoKQoKcF9yb2ZfcHJlZF9lcnJvcl9jb21wCgpnZ3NhdmUocGxvdCA9IHBfcm9mX3ByZWRfZXJyb3JfY29tcCwgZmlsZS5wYXRoKCIuLiIsICJvdXRwdXQiLCAicm9mX3ByZWRpY3Rpb25fZXJyb3JfY29tcGFyaXNvbnMucG5nIiksCiAgICAgICBkZXZpY2UgPSAicG5nIiwgd2lkdGggPSA3LjUsIGhlaWdodCA9IDUpCgpgYGAKCiMjIyMjIFN1bW1hcnkgcGxvdAoKYGBge3J9CiMgU2V0IHNpZ25pZmljYW5jZSBsZXZlbCBvZiBjb21wYXJpc29ucyBtYW51YWxseSwgYmFzZWQgb24gbW9kZWwgb3V0cHV0CnByZWRfZXJyb3Jfc3VtbWFyaXNlZCRjb21wYXJpc29uIDwtIGMoIioqKiIpCnByZWRfZXJyb3Jfc3VtbWFyaXNlZFtjKDMsIDgpLCBjb21wYXJpc29uIDo9IE5BXQpwcmVkX2Vycm9yX3N1bW1hcmlzZWRbYyg1LCAxMCksIGNvbXBhcmlzb24gOj0gIm4ucy4iXQoKIyBBZGQgZml0dGVkIHZhbHVlcwpwcmVkX2Vycm9yX3N1bW1hcmlzZWRbY291cnNlID09ICJGcmVuY2giLCBlcnJvcl9maXR0ZWQgOj0gcHJlZGljdChtX3JvZl9wcmVkX2Vycm9yX2dsLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhID0gcHJlZF9lcnJvcl9zdW1tYXJpc2VkW2NvdXJzZSA9PSAiRnJlbmNoIl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlLmZvcm0gPSBOQSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHR5cGUgPSAicmVzcG9uc2UiKV0KcHJlZF9lcnJvcl9zdW1tYXJpc2VkW2NvdXJzZSA9PSAiRW5nbGlzaCIsIGVycm9yX2ZpdHRlZCA6PSBwcmVkaWN0KG1fcHJlZF9lcnJvciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YSA9IHByZWRfZXJyb3Jfc3VtbWFyaXNlZFtjb3Vyc2UgPT0gIkVuZ2xpc2giXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmUuZm9ybSA9IE5BLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJyZXNwb25zZSIpXQoKcF9yb2ZfYWJzX3ByZWRfZXJyb3Jfc3VtbSA8LSBnZ3Bsb3QocHJlZF9lcnJvcl9zdW1tYXJpc2VkLCBhZXMoeCA9IHJlb3JkZXIocHJlZGljdGlvbl9sYWJlbCwgLWVycm9yX21lYW4pLCB5ID0gZXJyb3JfbWVhbiwgY29sb3VyID0gY291cnNlKSkgKwogIGdlb21fZXJyb3JiYXIoYWVzKHltaW4gPSBlcnJvcl9tZWFuIC0gZXJyb3Jfc2UsIHltYXggPSBlcnJvcl9tZWFuICsgZXJyb3Jfc2UpLCB3aWR0aCA9IDApICsKICBnZW9tX2xpbmUoYWVzKGdyb3VwID0gY291cnNlKSwgbHR5ID0gMikgKwogIGdlb21fcG9pbnQoKSArCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IGNvbXBhcmlzb24pLCAKICAgICAgICAgICAgY29sb3VyID0gImJsYWNrIiwKICAgICAgICAgICAgcG9zaXRpb24gPSBwb3NpdGlvbl9udWRnZSh4ID0gLjUsIHkgPSBjKHJlcCgwLCA5KSwgLjAwMSkpLAogICAgICAgICAgICBoanVzdCA9IC41KSArCiAgbGFicyh4ID0gIk1ldGhvZCIsCiAgICAgICB5ID0gIkFic29sdXRlIFNvRiBwcmVkaWN0aW9uIGVycm9yIiwKICAgICAgIGNvbG91ciA9ICJDb3Vyc2UiKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBkYXRhc2V0X2NvbG91cnMpICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSBjKC44NSwgLjg1KSkKCnBfcm9mX2Fic19wcmVkX2Vycm9yX3N1bW0KCmdnc2F2ZShwbG90ID0gcF9yb2ZfYWJzX3ByZWRfZXJyb3Jfc3VtbSwgZmlsZS5wYXRoKCIuLiIsICJvdXRwdXQiLCAicm9mX2Fic29sdXRlX3ByZWRpY3Rpb25fZXJyb3Jfc3VtbWFyeS5wbmciKSwKICAgICAgIGRldmljZSA9ICJwbmciLCB3aWR0aCA9IDcuNSwgaGVpZ2h0ID0gNC41KQpgYGAKCgpgYGB7cn0KcHJlZF9lcnJvcl9zdW1tYXJpc2VkWywgcHJlZGljdGlvbl9yYW5rIDo9IGZyYW5rKC1lcnJvcl9tZWFuKSwgYnkgPSAuKGNvdXJzZSldCgphbm5vdGF0aW9uX2RmX3NzIDwtIGRhdGEudGFibGUoCiAgY291cnNlID0gcmVwKCJFbmdsaXNoIiwgMTApLAogIHN0YXJ0ID0gYygxLCAxLCAxLCAxLAogICAgICAgICAgICAyLCAyLCAyLAogICAgICAgICAgICAzLCAzLAogICAgICAgICAgICA0CiAgKSwKICBlbmQgPSBjKDIsIDMsIDQsIDUsCiAgICAgICAgICAzLCA0LCA1LAogICAgICAgICAgNCwgNSwKICAgICAgICAgIDUKICApLAogIHkgPSBzZXEobWF4KHByZWRfZXJyb3Jfc3VtbWFyaXNlZCRlcnJvcl9tZWFuKSoxLjAxICsgLjAwNjc1LCBtYXgocHJlZF9lcnJvcl9zdW1tYXJpc2VkJGVycm9yX21lYW4pKjEuMDEsIGJ5ID0gLS4wMDA3NSksCiAgbGFiZWwgPSBjKCJwIDwgLjAwMSIsICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsCiAgICAgICAgICAgICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsCiAgICAgICAgICAgICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsCiAgICAgICAgICAgICJuLnMuIikKKQoKYW5ub3RhdGlvbl9kZl9nbCA8LSBkYXRhLnRhYmxlKAogIGNvdXJzZSA9IHJlcCgiRnJlbmNoIiwgMTApLAogIHN0YXJ0ID0gYygxLCAxLCAxLCAxLAogICAgICAgICAgICAyLCAyLCAyLAogICAgICAgICAgICAzLCAzLAogICAgICAgICAgICA0CiAgKSwKICBlbmQgPSBjKDIsIDMsIDQsIDUsCiAgICAgICAgICAzLCA0LCA1LAogICAgICAgICAgNCwgNSwKICAgICAgICAgIDUKICApLAogIHkgPSBzZXEobWF4KHByZWRfZXJyb3Jfc3VtbWFyaXNlZCRlcnJvcl9tZWFuKSoxLjAxICsgLjAwNjc1LCBtYXgocHJlZF9lcnJvcl9zdW1tYXJpc2VkJGVycm9yX21lYW4pKjEuMDEsIGJ5ID0gLS4wMDA3NSksCiAgbGFiZWwgPSBjKCJwIDwgLjAwMSIsICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsCiAgICAgICAgICAgICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsCiAgICAgICAgICAgICJwIDwgLjAwMSIsICJwIDwgLjAwMSIsCiAgICAgICAgICAgICJuLnMuIikKKQoKYW5ub3RhdGlvbl9kZl9yb2YgPC0gcmJpbmQoYW5ub3RhdGlvbl9kZl9zcywgYW5ub3RhdGlvbl9kZl9nbCkKYW5ub3RhdGlvbl9kZl9yb2ZbLCBsYWJlbCA6PSBmYWN0b3IobGFiZWwsIGxldmVscyA9IGMoInAgPCAuMDAxIiwgInAgPCAuMDEiLCAicCA8IC4wNSIsICJuLnMuIikpXQoKcF9yb2ZfcHJlZF9lcnJvcl9zdW1tYXJ5IDwtIGdncGxvdChwcmVkX2Vycm9yX3N1bW1hcmlzZWQsIGFlcyh4ID0gcHJlZGljdGlvbl9yYW5rLCB5ID0gZXJyb3JfbWVhbikpICsKICBmYWNldF9ncmlkKH4gY291cnNlKSArCiAgZ2VvbV9saW5lKGRhdGEgPSBhbm5vdGF0aW9uX2RmX3JvZiwKICAgICAgICAgICAgYWVzKHggPSAxLCB5ID0gLjA1LCBsdHkgPSBsYWJlbCwgYWxwaGEgPSBsYWJlbCwgY29sb3VyID0gTlVMTCkpICsgIyBEdW1teSBsaW5lIHRvIGdldCBsZWdlbmQKICBnZW9tX2xpbmUoYWVzKGNvbG91ciA9IGNvdXJzZSwgZ3JvdXAgPSBjb3Vyc2UpKSArCiAgZ2VvbV9lcnJvcmJhcihhZXMoeW1pbiA9IGVycm9yX21lYW4gLSBlcnJvcl9zZSwgeW1heCA9IGVycm9yX21lYW4gKyBlcnJvcl9zZSksIHdpZHRoID0gMCkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91ciA9IGNvdXJzZSwgZ3JvdXAgPSBjb3Vyc2UpKSArCiAgZ2VvbV9sYWJlbChhZXMobGFiZWwgPSBwcmVkaWN0aW9uX2xhYmVsKSwgCiAgICAgICAgICAgICBjb2xvdXIgPSAiYmxhY2siLCAKICAgICAgICAgICAgIGFscGhhID0gLjksCiAgICAgICAgICAgICBsYWJlbC5zaXplID0gTkEsIAogICAgICAgICAgICAgbnVkZ2VfeSA9IC0uMDAyNSkgKwogIGxhYnMoeCA9IE5VTEwsCiAgICAgICB5ID0gIkFic29sdXRlIHByZWRpY3Rpb24gZXJyb3I6XG5TcGVlZCBvZiBGb3JnZXR0aW5nIM6xIiwKICAgICAgIGNvbG91ciA9ICJDb3Vyc2UiKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGV4cGFuZCA9IGV4cGFuc2lvbihhZGQgPSAuNzUpLCBicmVha3MgPSBOVUxMKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGxpbWl0cyA9IGMoMCwgTkEpKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBkYXRhc2V0X2NvbG91cnMpICsKICBzY2FsZV9saW5ldHlwZV9tYW51YWwodmFsdWVzID0gYygicCA8IC4wMDEiID0gMSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicCA8IC4wMSIgPSA1LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwIDwgLjA1IiA9IDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIm4ucy4iID0gMyksCiAgICAgICAgICAgICAgICAgICAgICAgIGRyb3AgPSBGQUxTRSwKICAgICAgICAgICAgICAgICAgICAgICAgbmFtZSA9ICJQYWlyd2lzZSBjb21wYXJpc29uOiIpICsKICBzY2FsZV9hbHBoYV9tYW51YWwodmFsdWVzID0gYygicCA8IC4wMDEiID0gMSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicCA8IC4wMSIgPSAuNzUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInAgPCAuMDUiID0gLjUsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJuLnMuIiA9IC4yNSksCiAgICAgICAgICAgICAgICAgICAgIGRyb3AgPSBGQUxTRSwKICAgICAgICAgICAgICAgICAgICAgbmFtZSA9ICJQYWlyd2lzZSBjb21wYXJpc29uOiIpICsKICBndWlkZXMoY29sb3VyID0gIm5vbmUiKSArCiAgZ2dzaWduaWY6Omdlb21fc2lnbmlmKGRhdGEgPSBhbm5vdGF0aW9uX2RmX3JvZiwKICAgICAgICAgICAgICAgICAgICAgICAgYWVzKHhtaW4gPSBzdGFydCwgeG1heCA9IGVuZCwgYW5ub3RhdGlvbnMgPSAiIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB5X3Bvc2l0aW9uID0geSwgbHR5ID0gbGFiZWwsIGFscGhhID0gbGFiZWwpLAogICAgICAgICAgICAgICAgICAgICAgICB0aXBfbGVuZ3RoID0gMCwKICAgICAgICAgICAgICAgICAgICAgICAgbWFudWFsID0gVFJVRSkgICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwKICAgICAgICBsZWdlbmQuanVzdGlmaWNhdGlvbiA9ICJyaWdodCIpCgpwX3JvZl9wcmVkX2Vycm9yX3N1bW1hcnkKCmdnc2F2ZShmaWxlLnBhdGgoIi4uIiwgIm91dHB1dCIsICJyb2ZfYWJzb2x1dGVfcHJlZGljdGlvbl9lcnJvcl9zdW1tYXJ5LnBuZyIpLAogICAgICAgZGV2aWNlID0gInBuZyIsIHdpZHRoID0gMTAsIGhlaWdodCA9IDgpCmBgYAoKIyMjIyBJbXByb3ZlbWVudAoKSG93IGJpZyB3YXMgdGhlIGltcHJvdmVtZW50IGZyb20gd29yc3QgdG8gYmVzdCBwcmVkaWN0aW9uIG1ldGhvZD8KCkZyZW5jaDoKYGBge3J9CiMgQWJzb2x1dGUgY2hhbmdlCmh0X2dsX3RpZHlbY29udHJhc3QgPT0gIkZhY3QgLSBEZWZhdWx0IiwgZXN0aW1hdGVbMV1dCgojICUgY2hhbmdlCnNjYWxlczo6cGVyY2VudCgKICBodF9nbF90aWR5W2NvbnRyYXN0ID09ICJGYWN0IC0gRGVmYXVsdCIsIGVzdGltYXRlWzFdXSAvIGZpeGVmKG1fcm9mX3ByZWRfZXJyb3JfZ2wpW1sxXV0sCiAgYWNjdXJhY3kgPSAuMSkKCiMgQXNzb2NpYXRlZCBzdGFuZGFyZGlzZWQgZWZmZWN0IHNpemUKZWZmX3NpemVfZ2xfdGlkeSA8LSBicm9vbTo6dGlkeShlZmZfc2l6ZV9nbCkgfD4gYXMuZGF0YS50YWJsZSgpCmVmZl9zaXplX2dsX3RpZHlbY29udHJhc3QgPT0gIkRlZmF1bHQgLSBGYWN0IiwgZXN0aW1hdGVdCmBgYAoKRW5nbGlzaDoKYGBge3J9CiMgQWJzb2x1dGUgY2hhbmdlCmh0X3NzX3RpZHlbY29udHJhc3QgPT0gIkZhY3QgJiBMZWFybmVyIC0gRGVmYXVsdCIsIGVzdGltYXRlWzFdXQoKIyAlIGNoYW5nZQpzY2FsZXM6OnBlcmNlbnQoCiAgaHRfc3NfdGlkeVtjb250cmFzdCA9PSAiRmFjdCAmIExlYXJuZXIgLSBEZWZhdWx0IiwgZXN0aW1hdGVbMV1dIC8gZml4ZWYobV9wcmVkX2Vycm9yKVtbMV1dLAogIGFjY3VyYWN5ID0gLjEpCgojIEFzc29jaWF0ZWQgc3RhbmRhcmRpc2VkIGVmZmVjdCBzaXplCmVmZl9zaXplX3NzX3RpZHkgPC0gYnJvb206OnRpZHkoZWZmX3NpemVfc3MpIHw+IGFzLmRhdGEudGFibGUoKQplZmZfc2l6ZV9zc190aWR5W2NvbnRyYXN0ID09ICJEZWZhdWx0IC0gRmFjdCAmIExlYXJuZXIiLCBlc3RpbWF0ZV0KYGBgCgoKIyMjIFZpc3VhbGlzZSBwcmVkaWN0aW9uIGVycm9yCgojIyMjIEJ5IGxlYXJuZXIKCmBgYHtyfQp1c2VyX2ZyZXEgPC0gcHJlZF9ib3RoWywgLk4sIGJ5ID0gLihjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwsIHVzZXJfaWQpXQoKcHJlZF91c2VyX2ZyZXEgPC0gcHJlZF9ib3RoW3VzZXJfZnJlcVtOID4gNTBdLCBvbiA9IC4oY291cnNlLCBwcmVkaWN0aW9uX2xhYmVsLCB1c2VyX2lkKV0KCnByZWRfdXNlcl9xIDwtIHByZWRfdXNlcl9mcmVxWywgLihzdGF0ID0gYygid2hpc2tlcl9sb3ciLCAicTI1IiwgIm1lZGlhbiIsICJxNzUiLCAid2hpc2tlcl9oaWdoIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IGJveHBsb3Quc3RhdHMocHJlZGljdGlvbl9lcnJvciwgZG8uY29uZiA9IEZBTFNFLCBkby5vdXQgPSBGQUxTRSkkc3RhdHMpLCBieSA9IC4oY291cnNlLCBwcmVkaWN0aW9uX2xhYmVsLCB1c2VyX2lkKV0KCnByZWRfdXNlcl9xIDwtIHBpdm90X3dpZGVyKHByZWRfdXNlcl9xLCBuYW1lc19mcm9tID0gInN0YXQiLCB2YWx1ZXNfZnJvbSA9ICJ2YWx1ZSIpCgpwcmVkX3VzZXJfcSA8LSBwcmVkX3VzZXJfcSAlPiUKICBhcnJhbmdlKGNvdXJzZSwgcHJlZGljdGlvbl9sYWJlbCwgbWVkaWFuKSAlPiUKICBncm91cF9ieShjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwpICU+JQogIG11dGF0ZSh1c2VyX29yZGVyID0gKDE6bigpKS9uKCkpCmBgYAoKYGBge3J9CmdncGxvdChwcmVkX3VzZXJfcSwgYWVzKHggPSB1c2VyX29yZGVyKSkgKwogIGZhY2V0X2dyaWQoY291cnNlIH4gcHJlZGljdGlvbl9sYWJlbCkgKwogIGdlb21fcmliYm9uKGFlcyh5bWluID0gd2hpc2tlcl9sb3csIHltYXggPSB3aGlza2VyX2hpZ2gsIGZpbGwgPSBjb3Vyc2UpLCBhbHBoYSA9IC4zKSArCiAgZ2VvbV9yaWJib24oYWVzKHltaW4gPSBxMjUsIHltYXggPSBxNzUsIGZpbGwgPSBjb3Vyc2UpLCBhbHBoYSA9IC41KSArCiAgZ2VvbV9saW5lKGFlcyh5ID0gbWVkaWFuKSwgbHdkID0gMSkgKwogIGdlb21faGxpbmUoZGF0YSA9IE5VTEwsIHlpbnRlcmNlcHQgPSAwLCBsdHkgPSAzKSArCiAgICBsYWJzKHggPSAiTGVhcm5lcnMiLAogICAgICAgeSA9ICJTb0YgcHJlZGljdGlvbiBlcnJvclxuKHByZWRpY3RlZCAtIG9ic2VydmVkKSIpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBkYXRhc2V0X2NvbG91cnMpICsKICBndWlkZXMoZmlsbCA9ICJub25lIikgKwogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLAogICAgYXhpcy50aWNrcy54ID0gZWxlbWVudF9ibGFuaygpLAogICAgcGFuZWwuZ3JpZC5tYWpvci54ID0gZWxlbWVudF9ibGFuaygpKSArCiAgTlVMTAoKZ2dzYXZlKGZpbGUucGF0aCgiLi4iLCAib3V0cHV0IiwgInJvZl9wcmVkaWN0aW9uX2Vycm9yX2J5X2xlYXJuZXIucG5nIiksCiAgZGV2aWNlID0gInBuZyIsIHdpZHRoID0gMTAsIGhlaWdodCA9IDQuNSkKYGBgCgpBYnNvbHV0ZSBlcnJvcjoKYGBge3J9CnByZWRfdXNlcl9xIDwtIHByZWRfdXNlcl9mcmVxWywgLihzdGF0ID0gYygid2hpc2tlcl9sb3ciLCAicTI1IiwgIm1lZGlhbiIsICJxNzUiLCAid2hpc2tlcl9oaWdoIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IGJveHBsb3Quc3RhdHMoYWJzX3ByZWRpY3Rpb25fZXJyb3IsIGRvLmNvbmYgPSBGQUxTRSwgZG8ub3V0ID0gRkFMU0UpJHN0YXRzKSwgYnkgPSAuKGNvdXJzZSwgcHJlZGljdGlvbl9sYWJlbCwgdXNlcl9pZCldCgpwcmVkX3VzZXJfcSA8LSBwaXZvdF93aWRlcihwcmVkX3VzZXJfcSwgbmFtZXNfZnJvbSA9ICJzdGF0IiwgdmFsdWVzX2Zyb20gPSAidmFsdWUiKQoKcHJlZF91c2VyX3EgPC0gcHJlZF91c2VyX3EgJT4lCiAgYXJyYW5nZShjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwsIG1lZGlhbikgJT4lCiAgZ3JvdXBfYnkoY291cnNlLCBwcmVkaWN0aW9uX2xhYmVsKSAlPiUKICBtdXRhdGUodXNlcl9vcmRlciA9ICgxOm4oKSkvbigpKQoKZ2dwbG90KHByZWRfdXNlcl9xLCBhZXMoeCA9IHVzZXJfb3JkZXIpKSArCiAgZmFjZXRfZ3JpZChjb3Vyc2UgfiBwcmVkaWN0aW9uX2xhYmVsKSArCiAgZ2VvbV9yaWJib24oYWVzKHltaW4gPSB3aGlza2VyX2xvdywgeW1heCA9IHdoaXNrZXJfaGlnaCwgZmlsbCA9IGNvdXJzZSksIGFscGhhID0gLjMpICsKICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHEyNSwgeW1heCA9IHE3NSwgZmlsbCA9IGNvdXJzZSksIGFscGhhID0gLjUpICsKICBnZW9tX2xpbmUoYWVzKHkgPSBtZWRpYW4pLCBsd2QgPSAxKSArCiAgICBsYWJzKHggPSAiTGVhcm5lcnMiLAogICAgICAgeSA9ICJBYnNvbHV0ZSBTb0YgcHJlZGljdGlvbiBlcnJvciIpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBkYXRhc2V0X2NvbG91cnMpICsKICBndWlkZXMoZmlsbCA9ICJub25lIikgKwogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLAogICAgYXhpcy50aWNrcy54ID0gZWxlbWVudF9ibGFuaygpLAogICAgcGFuZWwuZ3JpZC5tYWpvci54ID0gZWxlbWVudF9ibGFuaygpKSArCiAgTlVMTAoKZ2dzYXZlKGZpbGUucGF0aCgiLi4iLCAib3V0cHV0IiwgInJvZl9hYnNfcHJlZGljdGlvbl9lcnJvcl9ieV9sZWFybmVyLnBuZyIpLAogIGRldmljZSA9ICJwbmciLCB3aWR0aCA9IDEwLCBoZWlnaHQgPSA0LjUpCmBgYAoKYGBge3J9CmdncGxvdChwcmVkX3VzZXJfcSwgYWVzKHggPSB1c2VyX29yZGVyLCBncm91cCA9IHByZWRpY3Rpb25fbGFiZWwsIGNvbG91ciA9IHByZWRpY3Rpb25fbGFiZWwpKSArCiAgZmFjZXRfZ3JpZCh+IGNvdXJzZSkgKwogIGdlb21fcmliYm9uKGFlcyh5bWluID0gcTI1LCB5bWF4ID0gcTc1LCBmaWxsID0gcHJlZGljdGlvbl9sYWJlbCksIGNvbG91ciA9IE5BLCBhbHBoYSA9IC4xNSkgKwogIGdlb21fbGluZShhZXMoeSA9IG1lZGlhbiksIGx3ZCA9IDEpICsKICAgIGxhYnMoeCA9ICJMZWFybmVycyIsCiAgICAgICB5ID0gIkFic29sdXRlIFNvRiBwcmVkaWN0aW9uIGVycm9yIiwKICAgICAgIGNvbG91ciA9ICJQcmVkaWN0aW9uXG5tZXRob2QiLAogICAgICAgZmlsbCA9ICJQcmVkaWN0aW9uXG5tZXRob2QiKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjb25kaXRpb25fY29sb3VycykgKwogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGNvbmRpdGlvbl9jb2xvdXJzKSArCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X2JsYW5rKCksCiAgICBheGlzLnRpY2tzLnggPSBlbGVtZW50X2JsYW5rKCksCiAgICBwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2JsYW5rKCkpICsKICBOVUxMCgpnZ3NhdmUoZmlsZS5wYXRoKCIuLiIsICJvdXRwdXQiLCAicm9mX2Fic19wcmVkaWN0aW9uX2Vycm9yX2J5X2xlYXJuZXJfY29uZGVuc2VkLnBuZyIpLAogIGRldmljZSA9ICJwbmciLCB3aWR0aCA9IDEwLCBoZWlnaHQgPSA0LjUpCmBgYAoKCiMjIyMgQnkgZmFjdApgYGB7cn0KZmFjdF9mcmVxIDwtIHByZWRfYm90aFssIC5OLCBieSA9IC4oY291cnNlLCBwcmVkaWN0aW9uX2xhYmVsLCBmYWN0X2lkKV0KCnByZWRfZmFjdF9mcmVxIDwtIHByZWRfYm90aFtmYWN0X2ZyZXFbTiA+IDUwXSwgb24gPSAuKGNvdXJzZSwgcHJlZGljdGlvbl9sYWJlbCwgZmFjdF9pZCldCgpwcmVkX2ZhY3RfcSA8LSBwcmVkX2ZhY3RfZnJlcVssIC4oc3RhdCA9IGMoIndoaXNrZXJfbG93IiwgInEyNSIsICJtZWRpYW4iLCAicTc1IiwgIndoaXNrZXJfaGlnaCIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBib3hwbG90LnN0YXRzKHByZWRpY3Rpb25fZXJyb3IsIGRvLmNvbmYgPSBGQUxTRSwgZG8ub3V0ID0gRkFMU0UpJHN0YXRzKSwgYnkgPSAuKGNvdXJzZSwgcHJlZGljdGlvbl9sYWJlbCwgZmFjdF9pZCldCgpwcmVkX2ZhY3RfcSA8LSBwaXZvdF93aWRlcihwcmVkX2ZhY3RfcSwgbmFtZXNfZnJvbSA9ICJzdGF0IiwgdmFsdWVzX2Zyb20gPSAidmFsdWUiKQoKcHJlZF9mYWN0X3EgPC0gcHJlZF9mYWN0X3EgJT4lCiAgYXJyYW5nZShjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwsIG1lZGlhbikgJT4lCiAgZ3JvdXBfYnkoY291cnNlLCBwcmVkaWN0aW9uX2xhYmVsKSAlPiUKICBtdXRhdGUoZmFjdF9vcmRlciA9ICgxOm4oKSkvbigpKQpgYGAKCmBgYHtyfQpnZ3Bsb3QocHJlZF9mYWN0X3EsIGFlcyh4ID0gZmFjdF9vcmRlcikpICsKICBmYWNldF9ncmlkKGNvdXJzZSB+IHByZWRpY3Rpb25fbGFiZWwpICsKICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHdoaXNrZXJfbG93LCB5bWF4ID0gd2hpc2tlcl9oaWdoLCBmaWxsID0gY291cnNlKSwgYWxwaGEgPSAuMykgKwogIGdlb21fcmliYm9uKGFlcyh5bWluID0gcTI1LCB5bWF4ID0gcTc1LCBmaWxsID0gY291cnNlKSwgYWxwaGEgPSAuNSkgKwogIGdlb21fbGluZShhZXMoeSA9IG1lZGlhbiksIGx3ZCA9IDEpICsKICBnZW9tX2hsaW5lKGRhdGEgPSBOVUxMLCB5aW50ZXJjZXB0ID0gMCwgbHR5ID0gMykgKwogICAgbGFicyh4ID0gIkZhY3RzIiwKICAgICAgIHkgPSAiU29GIHByZWRpY3Rpb24gZXJyb3JcbihwcmVkaWN0ZWQgLSBvYnNlcnZlZCkiKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gZGF0YXNldF9jb2xvdXJzKSArCiAgZ3VpZGVzKGZpbGwgPSAibm9uZSIpICsKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSwKICAgIGF4aXMudGlja3MueCA9IGVsZW1lbnRfYmxhbmsoKSwKICAgIHBhbmVsLmdyaWQubWFqb3IueCA9IGVsZW1lbnRfYmxhbmsoKSkgKwogIE5VTEwKCmdnc2F2ZShmaWxlLnBhdGgoIi4uIiwgIm91dHB1dCIsICJyb2ZfcHJlZGljdGlvbl9lcnJvcl9ieV9mYWN0LnBuZyIpLAogIGRldmljZSA9ICJwbmciLCB3aWR0aCA9IDEwLCBoZWlnaHQgPSA0LjUpCmBgYAoKQWJzb2x1dGUgZXJyb3I6CmBgYHtyfQpwcmVkX2ZhY3RfcSA8LSBwcmVkX2ZhY3RfZnJlcVssIC4oc3RhdCA9IGMoIndoaXNrZXJfbG93IiwgInEyNSIsICJtZWRpYW4iLCAicTc1IiwgIndoaXNrZXJfaGlnaCIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBib3hwbG90LnN0YXRzKGFic19wcmVkaWN0aW9uX2Vycm9yLCBkby5jb25mID0gRkFMU0UsIGRvLm91dCA9IEZBTFNFKSRzdGF0cyksIGJ5ID0gLihjb3Vyc2UsIHByZWRpY3Rpb25fbGFiZWwsIGZhY3RfaWQpXQoKcHJlZF9mYWN0X3EgPC0gcGl2b3Rfd2lkZXIocHJlZF9mYWN0X3EsIG5hbWVzX2Zyb20gPSAic3RhdCIsIHZhbHVlc19mcm9tID0gInZhbHVlIikKCnByZWRfZmFjdF9xIDwtIHByZWRfZmFjdF9xICU+JQogIGFycmFuZ2UoY291cnNlLCBwcmVkaWN0aW9uX2xhYmVsLCBtZWRpYW4pICU+JQogIGdyb3VwX2J5KGNvdXJzZSwgcHJlZGljdGlvbl9sYWJlbCkgJT4lCiAgbXV0YXRlKGZhY3Rfb3JkZXIgPSAoMTpuKCkpL24oKSkKCmdncGxvdChwcmVkX2ZhY3RfcSwgYWVzKHggPSBmYWN0X29yZGVyKSkgKwogIGZhY2V0X2dyaWQoY291cnNlIH4gcHJlZGljdGlvbl9sYWJlbCkgKwogIGdlb21fcmliYm9uKGFlcyh5bWluID0gd2hpc2tlcl9sb3csIHltYXggPSB3aGlza2VyX2hpZ2gsIGZpbGwgPSBjb3Vyc2UpLCBhbHBoYSA9IC4zKSArCiAgZ2VvbV9yaWJib24oYWVzKHltaW4gPSBxMjUsIHltYXggPSBxNzUsIGZpbGwgPSBjb3Vyc2UpLCBhbHBoYSA9IC41KSArCiAgZ2VvbV9saW5lKGFlcyh5ID0gbWVkaWFuKSwgbHdkID0gMSkgKwogICAgbGFicyh4ID0gIkZhY3RzIiwKICAgICAgIHkgPSAiQWJzb2x1dGUgU29GIHByZWRpY3Rpb24gZXJyb3IiKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gZGF0YXNldF9jb2xvdXJzKSArCiAgZ3VpZGVzKGZpbGwgPSAibm9uZSIpICsKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSwKICAgIGF4aXMudGlja3MueCA9IGVsZW1lbnRfYmxhbmsoKSwKICAgIHBhbmVsLmdyaWQubWFqb3IueCA9IGVsZW1lbnRfYmxhbmsoKSkgKwogIE5VTEwKCmdnc2F2ZShmaWxlLnBhdGgoIi4uIiwgIm91dHB1dCIsICJyb2ZfYWJzX3ByZWRpY3Rpb25fZXJyb3JfYnlfZmFjdC5wbmciKSwKICBkZXZpY2UgPSAicG5nIiwgd2lkdGggPSAxMCwgaGVpZ2h0ID0gNC41KQpgYGAKCmBgYHtyfQpnZ3Bsb3QocHJlZF9mYWN0X3EsIGFlcyh4ID0gZmFjdF9vcmRlciwgZ3JvdXAgPSBwcmVkaWN0aW9uX2xhYmVsLCBjb2xvdXIgPSBwcmVkaWN0aW9uX2xhYmVsKSkgKwogIGZhY2V0X2dyaWQofiBjb3Vyc2UpICsKICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHEyNSwgeW1heCA9IHE3NSwgZmlsbCA9IHByZWRpY3Rpb25fbGFiZWwpLCBjb2xvdXIgPSBOQSwgYWxwaGEgPSAuMTUpICsKICBnZW9tX2xpbmUoYWVzKHkgPSBtZWRpYW4pLCBsd2QgPSAxKSArCiAgICBsYWJzKHggPSAiRmFjdHMiLAogICAgICAgeSA9ICJBYnNvbHV0ZSBTb0YgcHJlZGljdGlvbiBlcnJvciIsCiAgICAgICBjb2xvdXIgPSAiUHJlZGljdGlvblxubWV0aG9kIiwKICAgICAgIGZpbGwgPSAiUHJlZGljdGlvblxubWV0aG9kIikgKwogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gY29uZGl0aW9uX2NvbG91cnMpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjb25kaXRpb25fY29sb3VycykgKwogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLAogICAgYXhpcy50aWNrcy54ID0gZWxlbWVudF9ibGFuaygpLAogICAgcGFuZWwuZ3JpZC5tYWpvci54ID0gZWxlbWVudF9ibGFuaygpKSArCiAgTlVMTAoKZ2dzYXZlKGZpbGUucGF0aCgiLi4iLCAib3V0cHV0IiwgInJvZl9hYnNfcHJlZGljdGlvbl9lcnJvcl9ieV9mYWN0X2NvbmRlbnNlZC5wbmciKSwKICBkZXZpY2UgPSAicG5nIiwgd2lkdGggPSAxMCwgaGVpZ2h0ID0gNC41KQpgYGAKCgpgYGB7cn0KZ2dwbG90KHByZWRfYm90aCwgYWVzKHggPSBhYnNfcHJlZGljdGlvbl9lcnJvciwgY29sb3VyID0gcHJlZGljdGlvbl9sYWJlbCwgZmlsbCA9IHByZWRpY3Rpb25fbGFiZWwpKSArCiAgZmFjZXRfZ3JpZCh+IGNvdXJzZSkgKwogIGdlb21fZGVuc2l0eShhbHBoYSA9IC4xKSArCiAgICAgIGxhYnMoeCA9ICJBYnNvbHV0ZSBTb0YgcHJlZGljdGlvbiBlcnJvciIsCiAgICAgICB5ID0gIkRlbnNpdHkiLAogICAgICAgY29sb3VyID0gIlByZWRpY3Rpb25cbm1ldGhvZCIsCiAgICAgICBmaWxsID0gIlByZWRpY3Rpb25cbm1ldGhvZCIpICsKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGNvbmRpdGlvbl9jb2xvdXJzKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gY29uZGl0aW9uX2NvbG91cnMpICsKICBjb29yZF9jYXJ0ZXNpYW4oeWxpbSA9IGMoMCwgMTAwKSwgeGxpbSA9IGMoMCwgLjI1KSkgKwogIE5VTEwKCmdnc2F2ZShmaWxlLnBhdGgoIi4uIiwgIm91dHB1dCIsICJyb2ZfYWJzX3ByZWRpY3Rpb25fZXJyb3JfZGVuc2l0eS5wbmciKSwKICBkZXZpY2UgPSAicG5nIiwgd2lkdGggPSAxMCwgaGVpZ2h0ID0gNC41KQpgYGAKCgoKCgojIFNlc3Npb24gaW5mbwoKYGBge3J9CnNlc3Npb25JbmZvKCkKYGBgCgo=